From b26c8199a5048569674898755bcad95adb41bb2e Mon Sep 17 00:00:00 2001 From: Kunthawat Greethong Date: Thu, 16 Apr 2026 17:40:27 +0700 Subject: [PATCH] Update skills: add website-creator, mql-developer, ecommerce-astro Changes: - Add FAL_KEY and GEMINI_API_KEY to .env.example - Update picture-it to use ~/.config/opencode/.env (unified creds) - Remove shodh-memory skill (no longer used) - Remove alphaear-* skills (deprecated) - Remove thai-frontend-dev skill (replaced by website-creator) - Remove theme-factory skill - Add mql-developer skill (MQL5 trading) - Add ecommerce-astro skill (Astro e-commerce) - Add website-creator skill (Next.js + Payload CMS) - Update install script for new skills --- .DS_Store | Bin 8196 -> 8196 bytes .env.example | 18 +- .opencode/package-lock.json | 115 + output/test/results.json | 90 - output/บรการ-podcast-hosting/results.json | 437 --- scripts/install-skills.sh | 9 + skills/.DS_Store | Bin 6148 -> 10244 bytes skills/alphaear-deepear-lite/SKILL.md | 32 - .../scripts/deepear_lite.py | 112 - skills/alphaear-logic-visualizer/SKILL.md | 31 - .../references/PROMPTS.md | 52 - .../scripts/__init__.py | 0 .../scripts/visualizer.py | 472 ---- .../scripts/visualizer_prompt.py | 47 - .../tests/test_visualizer.py | 21 - skills/alphaear-news/SKILL.md | 33 - skills/alphaear-news/references/sources.md | 26 - skills/alphaear-news/scripts/__init__.py | 0 .../scripts/content_extractor.py | 122 - .../alphaear-news/scripts/database_manager.py | 131 - skills/alphaear-news/scripts/news_tools.py | 256 -- skills/alphaear-news/tests/test_news.py | 24 - skills/alphaear-predictor/SKILL.md | 60 - .../alphaear-predictor/references/PROMPTS.md | 43 - skills/alphaear-predictor/scripts/__init__.py | 0 .../scripts/forecast_agent.py | 76 - .../alphaear-predictor/scripts/json_utils.py | 180 -- .../scripts/kronos_predictor.py | 218 -- .../models/kronos_news_v1_20260101_0015.pt | Bin 1283533 -> 0 bytes .../scripts/predictor/model/__init__.py | 16 - .../scripts/predictor/model/kronos.py | 676 ----- .../scripts/predictor/model/module.py | 562 ---- .../scripts/prompts/fin_agent.py | 127 - .../scripts/prompts/forecast_analyst.py | 49 - .../scripts/prompts/intent_agent.py | 45 - .../scripts/prompts/isq_prompt_generator.py | 43 - .../scripts/prompts/report_agent.py | 415 --- .../scripts/prompts/trend_agent.py | 156 -- .../scripts/prompts/visualizer.py | 47 - .../scripts/schema/isq_template.py | 381 --- .../scripts/schema/models.py | 100 - .../scripts/utils/__init__.py | 1 - .../scripts/utils/database_manager.py | 581 ---- .../scripts/utils/json_utils.py | 180 -- .../scripts/utils/llm/capability.py | 85 - .../scripts/utils/llm/factory.py | 122 - .../scripts/utils/llm/router.py | 80 - .../scripts/utils/logging_setup.py | 45 - .../scripts/utils/predictor/evaluation.py | 137 - .../scripts/utils/predictor/kline_generate.py | 196 -- .../scripts/utils/predictor/model/__init__.py | 16 - .../scripts/utils/predictor/model/kronos.py | 676 ----- .../scripts/utils/predictor/model/module.py | 562 ---- .../scripts/utils/predictor/training.py | 539 ---- .../scripts/utils/search_tools.py | 611 ----- .../scripts/utils/stock_tools.py | 257 -- .../tests/test_predictor.py | 29 - skills/alphaear-reporter/SKILL.md | 32 - .../alphaear-reporter/references/PROMPTS.md | 77 - skills/alphaear-reporter/scripts/__init__.py | 0 .../scripts/prompts/fin_agent.py | 127 - .../scripts/prompts/forecast_analyst.py | 49 - .../scripts/prompts/intent_agent.py | 45 - .../scripts/prompts/isq_prompt_generator.py | 43 - .../scripts/prompts/report_agent.py | 415 --- .../scripts/prompts/trend_agent.py | 156 -- .../scripts/prompts/visualizer.py | 47 - .../alphaear-reporter/scripts/report_agent.py | 167 -- .../scripts/schema/isq_template.py | 381 --- .../scripts/schema/models.py | 100 - .../scripts/tools/__init__.py | 24 - .../scripts/tools/toolkits.py | 526 ---- .../scripts/utils/__init__.py | 1 - .../scripts/utils/content_extractor.py | 122 - .../scripts/utils/database_manager.py | 581 ---- .../scripts/utils/hybrid_search.py | 216 -- .../scripts/utils/json_utils.py | 180 -- .../scripts/utils/llm/capability.py | 85 - .../scripts/utils/llm/factory.py | 114 - .../scripts/utils/llm/router.py | 80 - .../scripts/utils/logging_setup.py | 45 - .../scripts/utils/news_tools.py | 256 -- .../scripts/utils/predictor/evaluation.py | 137 - .../scripts/utils/predictor/kline_generate.py | 196 -- .../scripts/utils/predictor/model/__init__.py | 16 - .../scripts/utils/predictor/model/kronos.py | 676 ----- .../scripts/utils/predictor/model/module.py | 562 ---- .../scripts/utils/predictor/training.py | 539 ---- .../scripts/utils/search_tools.py | 611 ----- .../scripts/utils/sentiment_tools.py | 287 -- .../scripts/utils/stock_tools.py | 257 -- .../alphaear-reporter/scripts/visualizer.py | 472 ---- .../alphaear-reporter/tests/test_reporter.py | 29 - skills/alphaear-search/SKILL.md | 35 - skills/alphaear-search/references/PROMPTS.md | 20 - skills/alphaear-search/scripts/__init__.py | 0 .../scripts/content_extractor.py | 122 - .../scripts/database_manager.py | 159 -- .../alphaear-search/scripts/hybrid_search.py | 216 -- .../alphaear-search/scripts/llm/__init__.py | 0 .../alphaear-search/scripts/llm/capability.py | 85 - skills/alphaear-search/scripts/llm/factory.py | 114 - skills/alphaear-search/scripts/llm/router.py | 80 - .../alphaear-search/scripts/search_tools.py | 479 ---- .../scripts/sentiment_tools.py | 287 -- skills/alphaear-search/tests/test_search.py | 31 - skills/alphaear-sentiment/SKILL.md | 57 - skills/alphaear-sentiment/scripts/__init__.py | 0 .../scripts/database_manager.py | 581 ---- .../scripts/llm/capability.py | 85 - .../alphaear-sentiment/scripts/llm/factory.py | 114 - .../alphaear-sentiment/scripts/llm/router.py | 80 - .../scripts/sentiment_tools.py | 205 -- .../tests/test_sentiment.py | 25 - skills/alphaear-signal-tracker/SKILL.md | 51 - .../references/PROMPTS.md | 72 - .../scripts/__init__.py | 0 .../scripts/fin_agent.py | 106 - .../scripts/prompts/fin_agent.py | 127 - .../scripts/prompts/forecast_analyst.py | 49 - .../scripts/prompts/intent_agent.py | 45 - .../scripts/prompts/isq_prompt_generator.py | 43 - .../scripts/prompts/report_agent.py | 415 --- .../scripts/prompts/trend_agent.py | 156 -- .../scripts/prompts/visualizer.py | 47 - .../scripts/schema/isq_template.py | 381 --- .../scripts/schema/models.py | 100 - .../scripts/tools/__init__.py | 24 - .../scripts/tools/toolkits.py | 526 ---- .../scripts/utils/__init__.py | 1 - .../scripts/utils/content_extractor.py | 122 - .../scripts/utils/database_manager.py | 581 ---- .../scripts/utils/hybrid_search.py | 216 -- .../scripts/utils/json_utils.py | 180 -- .../scripts/utils/llm/capability.py | 85 - .../scripts/utils/llm/factory.py | 114 - .../scripts/utils/llm/router.py | 80 - .../scripts/utils/logging_setup.py | 45 - .../scripts/utils/md_to_html.py | 185 -- .../scripts/utils/news_tools.py | 256 -- .../scripts/utils/predictor/evaluation.py | 137 - .../scripts/utils/predictor/kline_generate.py | 196 -- .../scripts/utils/predictor/model/__init__.py | 16 - .../scripts/utils/predictor/model/kronos.py | 676 ----- .../scripts/utils/predictor/model/module.py | 562 ---- .../scripts/utils/predictor/training.py | 539 ---- .../scripts/utils/search_tools.py | 611 ----- .../scripts/utils/sentiment_tools.py | 287 -- .../scripts/utils/stock_tools.py | 257 -- .../tests/test_tracker.py | 22 - skills/alphaear-stock/SKILL.md | 28 - skills/alphaear-stock/scripts/__init__.py | 0 .../scripts/database_manager.py | 119 - skills/alphaear-stock/scripts/stock_tools.py | 419 --- skills/alphaear-stock/tests/test_stock.py | 24 - skills/ecommerce-astro/SKILL.md | 620 +++++ skills/ecommerce-astro/scripts/.env.example | 17 + .../scripts/create_ecommerce.py | 2034 ++++++++++++++ .../ecommerce-astro/scripts/requirements.txt | 3 + .../scripts/supabase_migration.sql | 421 +++ .../scripts/templates/Dockerfile | 39 + .../scripts/templates/docker-compose.yml | 39 + .../src/components/admin/AdminSidebar.astro | 63 + .../src/components/admin/AdminSidebar.tsx | 151 + .../src/components/admin/DataTable.tsx | 227 ++ .../components/admin/OrderStatusManager.tsx | 246 ++ .../src/components/cart/AddToCart.tsx | 77 + .../src/components/cart/CartBadge.tsx | 18 + .../src/components/cart/CartDrawer.tsx | 77 + .../src/components/cart/CartItems.tsx | 50 + .../src/components/cart/CartSummary.tsx | 35 + .../src/components/checkout/BillingInfo.tsx | 72 + .../src/components/checkout/CheckoutForm.tsx | 81 + .../checkout/CheckoutSingleItem.tsx | 29 + .../checkout/CheckoutSingleItemDark.tsx | 29 + .../checkout/OrderStatusManager.tsx | 39 + .../src/components/checkout/OrderSummary.tsx | 64 + .../components/checkout/PaymentDetails.tsx | 57 + .../src/components/checkout/PaymentMethod.tsx | 72 + .../src/components/checkout/ShippingForm.tsx | 116 + .../components/incentives/IncentiveCols.tsx | 25 + .../components/incentives/IncentiveLarge.tsx | 54 + .../src/components/layout/Footer.astro | 103 + .../src/components/layout/Header.astro | 113 + .../src/components/order/OrderCardProduct.tsx | 52 + .../src/components/order/OrderHistory.tsx | 68 + .../src/components/order/OrderHistoryCard.tsx | 58 + .../src/components/order/OrderProductRow.tsx | 26 + .../src/components/order/OrderSummaries.tsx | 81 + .../src/components/product/CardCategory.tsx | 30 + .../components/product/ProductAccordion.tsx | 39 + .../src/components/product/ProductBadge.tsx | 37 + .../src/components/product/ProductCard.astro | 101 + .../src/components/product/ProductFeature.tsx | 49 + .../components/product/ProductFeature2.tsx | 57 + .../src/components/product/ProductFilters.tsx | 108 + .../src/components/product/ProductGallery.tsx | 124 + .../product/ProductOverviewGallery.tsx | 124 + .../product/ProductOverviewGrid.tsx | 133 + .../components/product/ProductQuickview2.tsx | 104 + .../src/components/product/ProductRating.tsx | 36 + .../src/components/product/ProductSizes.tsx | 36 + .../components/product/ProductVariants.tsx | 103 + .../src/components/product/StockBadge.tsx | 63 + .../src/components/product/WishlistButton.tsx | 90 + .../components/promo/PromoSectionLarge.tsx | 47 + .../src/components/promo/TestimonialsFade.tsx | 92 + .../src/components/review/ReviewComment.tsx | 61 + .../src/components/review/ReviewForm.tsx | 179 ++ .../src/components/review/ReviewList.tsx | 122 + .../src/components/review/ReviewProgress.tsx | 50 + .../src/components/review/StarRating.tsx | 59 + .../components/store/StoreDoubleColumn.tsx | 45 + .../src/components/store/StoreNavbar.tsx | 75 + .../src/components/store/StoreNavigation.tsx | 75 + .../src/components/store/UpperNavbar.tsx | 49 + .../templates/src/components/ui/Button.tsx | 65 + .../templates/src/components/ui/Input.tsx | 79 + .../templates/src/components/ui/Modal.tsx | 107 + .../templates/src/components/ui/Select.tsx | 91 + .../src/components/vendor/EarningsChart.tsx | 175 ++ .../src/components/vendor/ProductForm.tsx | 363 +++ .../src/components/vendor/VendorCard.tsx | 104 + .../scripts/templates/src/i18n/en.json | 116 + .../scripts/templates/src/i18n/th.json | 116 + .../templates/src/layouts/Layout.astro | 31 + .../scripts/templates/src/lib/auth.ts | 172 ++ .../scripts/templates/src/lib/db.ts | 437 +++ .../scripts/templates/src/lib/payso.ts | 287 ++ .../scripts/templates/src/lib/stripe.ts | 294 ++ .../scripts/templates/src/lib/supabase.ts | 402 +++ .../scripts/templates/src/lib/utils.ts | 419 +++ .../templates/src/pages/account/index.astro | 146 + .../src/pages/account/orders/[id].astro | 176 ++ .../src/pages/account/orders/index.astro | 93 + .../src/pages/admin/categories.astro | 69 + .../templates/src/pages/admin/dashboard.astro | 105 + .../templates/src/pages/admin/orders.astro | 79 + .../templates/src/pages/admin/users.astro | 93 + .../templates/src/pages/admin/vendors.astro | 109 + .../templates/src/pages/api/auth/login.ts | 74 + .../templates/src/pages/api/auth/logout.ts | 26 + .../templates/src/pages/api/auth/me.ts | 66 + .../templates/src/pages/api/auth/register.ts | 101 + .../src/pages/api/categories/index.ts | 41 + .../templates/src/pages/api/orders/[id].ts | 158 ++ .../templates/src/pages/api/orders/index.ts | 199 ++ .../src/pages/api/payments/callback.ts | 79 + .../src/pages/api/payments/create.ts | 108 + .../src/pages/api/payments/inquiry.ts | 121 + .../src/pages/api/products/[slug].ts | 65 + .../templates/src/pages/api/products/index.ts | 161 ++ .../templates/src/pages/api/vendors/[slug].ts | 61 + .../templates/src/pages/api/vendors/apply.ts | 98 + .../src/pages/api/vendors/dashboard.ts | 114 + .../templates/src/pages/api/vendors/index.ts | 57 + .../templates/src/pages/api/webhooks/payso.ts | 112 + .../scripts/templates/src/pages/cart.astro | 52 + .../templates/src/pages/checkout.astro | 25 + .../src/pages/checkout/success.astro | 75 + .../scripts/templates/src/pages/index.astro | 163 ++ .../scripts/templates/src/pages/login.astro | 114 + .../templates/src/pages/products/[slug].astro | 209 ++ .../templates/src/pages/products/index.astro | 127 + .../templates/src/pages/register.astro | 134 + .../src/pages/vendor/dashboard.astro | 143 + .../templates/src/pages/vendor/orders.astro | 108 + .../src/pages/vendor/products/[id]/edit.astro | 50 + .../src/pages/vendor/products/index.astro | 112 + .../src/pages/vendor/products/new.astro | 38 + .../templates/src/pages/vendor/settings.astro | 38 + .../templates/src/pages/vendors/[slug].astro | 110 + .../templates/src/pages/vendors/index.astro | 88 + .../templates/src/pages/wishlist.astro | 93 + .../scripts/templates/src/stores/auth.ts | 113 + .../scripts/templates/src/stores/cart.ts | 88 + .../scripts/templates/src/stores/vendor.ts | 138 + skills/mql-developer/LICENSE | 21 + skills/mql-developer/README.md | 73 + skills/mql-developer/SKILL.md | 393 +++ .../fibo-zone-indicator/SKILL.md | 118 + .../mql5-dashboard-survive-tf-change/SKILL.md | 129 + .../mql5-ea-troubleshooting/SKILL.md | 62 + .../references/architecture-patterns.md | 2426 +++++++++++++++++ .../mql-developer/references/backtesting.md | 237 ++ .../references/external-communication.md | 1057 +++++++ .../references/indicators-and-ui.md | 1062 ++++++++ .../references/mql4-reference.md | 2051 ++++++++++++++ .../references/mql5-reference.md | 1255 +++++++++ .../references/security-licensing.md | 259 ++ .../references/trading-operations.md | 1142 ++++++++ skills/shodh-memory/SKILL.md | 105 - skills/shodh-memory/scripts/.env.example | 9 - skills/shodh-memory/scripts/cli.py | 206 -- skills/shodh-memory/scripts/install.sh | 76 - skills/shodh-memory/scripts/requirements.txt | 2 - .../thai-frontend-dev/AUTO_ADMIN_PASSWORD.md | 119 - .../thai-frontend-dev/AUTO_DEPLOY_COMPLETE.md | 263 -- .../AUTO_DEPLOY_IMPLEMENTATION.md | 463 ---- .../thai-frontend-dev/AUTO_DEPLOY_PROGRESS.md | 131 - .../EASYPANEL_INTEGRATION.md | 309 --- skills/thai-frontend-dev/FINAL_SUMMARY.md | 410 --- .../IMPLEMENTATION_STATUS.md | 332 --- .../IMPLEMENTATION_SUMMARY.md | 457 ---- .../thai-frontend-dev/MIGRATION_WORKFLOW.md | 65 - skills/thai-frontend-dev/README.md | 99 - skills/thai-frontend-dev/SKILL.md | 829 ------ skills/thai-frontend-dev/SPECIFICATION.md | 934 ------- skills/thai-frontend-dev/TEST_REPORT.md | 357 --- skills/thai-frontend-dev/UPDATE_SUMMARY.md | 68 - skills/thai-frontend-dev/scripts/.env.example | 19 - .../scripts/create_astro_website.py | 1009 ------- .../scripts/migrate_existing_website.py | 561 ---- .../scripts/refactor_existing_website.py | 1284 --------- .../scripts/requirements.txt | 1 - .../templates/admin-consent-logs.astro | 382 --- .../templates/components/common/Footer.astro | 135 - .../templates/components/common/Header.astro | 122 - .../scripts/templates/icons/line.svg | 10 - .../templates/layouts/BaseLayout.astro | 190 -- .../scripts/templates/pages/index.astro | 183 -- .../scripts/templates/styles/global.css | 298 -- .../templates/thai-privacy-policy-template.md | 179 -- .../scripts/umami_integration.py | 213 -- skills/theme-factory/SKILL.md | 59 - skills/theme-factory/theme-showcase.pdf | Bin 124310 -> 0 bytes skills/theme-factory/themes/arctic-frost.md | 19 - .../theme-factory/themes/botanical-garden.md | 19 - skills/theme-factory/themes/desert-rose.md | 19 - skills/theme-factory/themes/forest-canopy.md | 19 - skills/theme-factory/themes/golden-hour.md | 19 - .../theme-factory/themes/midnight-galaxy.md | 19 - .../theme-factory/themes/modern-minimalist.md | 19 - skills/theme-factory/themes/ocean-depths.md | 19 - .../theme-factory/themes/sunset-boulevard.md | 19 - .../theme-factory/themes/tech-innovation.md | 19 - skills/website-creator/.DS_Store | Bin 0 -> 6148 bytes skills/website-creator/SKILL.md | 2271 +++++++++++++++ .../api-and-interface-design/SKILL.md | 294 ++ skills/website-creator/creative/.DS_Store | Bin 0 -> 6148 bytes .../creative/picture-it/SKILL.md | 139 + .../references/pipeline-templates.json | 49 + .../picture-it/scripts/thai-font-patch.ts | 268 ++ skills/website-creator/design/SKILL.md | 302 ++ .../design/data/cip/deliverables.csv | 51 + .../design/data/cip/industries.csv | 21 + .../design/data/cip/mockup-contexts.csv | 21 + .../design/data/cip/styles.csv | 21 + .../design/data/icon/styles.csv | 16 + .../design/data/logo/colors.csv | 56 + .../design/data/logo/industries.csv | 56 + .../design/data/logo/styles.csv | 56 + .../references/banner-sizes-and-styles.md | 118 + .../references/cip-deliverable-guide.md | 95 + .../design/references/cip-design.md | 121 + .../references/cip-prompt-engineering.md | 84 + .../design/references/cip-style-guide.md | 68 + .../design/references/design-routing.md | 207 ++ .../design/references/icon-design.md | 122 + .../references/logo-color-psychology.md | 101 + .../design/references/logo-design.md | 92 + .../references/logo-prompt-engineering.md | 158 ++ .../design/references/logo-style-guide.md | 109 + .../references/slides-copywriting-formulas.md | 84 + .../design/references/slides-create.md | 4 + .../design/references/slides-html-template.md | 295 ++ .../references/slides-layout-patterns.md | 137 + .../design/references/slides-strategies.md | 94 + .../design/references/slides.md | 42 + .../design/references/social-photos-design.md | 329 +++ .../design/scripts/cip/core.py | 215 ++ .../design/scripts/cip/generate.py | 484 ++++ .../design/scripts/cip/render-html.py | 424 +++ .../design/scripts/cip/search.py | 127 + .../design/scripts/icon/generate.py | 487 ++++ .../design/scripts/logo/core.py | 175 ++ .../design/scripts/logo/generate.py | 362 +++ .../design/scripts/logo/search.py | 114 + .../frontend-ui-engineering/SKILL.md | 322 +++ skills/website-creator/general/plan/SKILL.md | 57 + .../general/requesting-code-review/SKILL.md | 282 ++ .../skill-augmentation-from-source/SKILL.md | 173 ++ .../subagent-driven-development/SKILL.md | 377 +++ .../general/systematic-debugging/SKILL.md | 366 +++ .../general/test-driven-development/SKILL.md | 342 +++ .../general/writing-plans/SKILL.md | 296 ++ .../payload-lexical-integration/SKILL.md | 290 ++ .../payload-nextjs-turbopack-fix/SKILL.md | 183 ++ .../payload-v3-admin-init/SKILL.md | 62 + skills/website-creator/payload/SKILL.md | 448 +++ .../reference/ACCESS-CONTROL-ADVANCED.md | 704 +++++ .../payload/reference/ACCESS-CONTROL.md | 697 +++++ .../payload/reference/ADAPTERS.md | 326 +++ .../payload/reference/ADVANCED.md | 386 +++ .../payload/reference/COLLECTIONS.md | 303 ++ .../payload/reference/ENDPOINTS.md | 634 +++++ .../payload/reference/FIELD-TYPE-GUARDS.md | 553 ++++ .../payload/reference/FIELDS.md | 744 +++++ .../payload/reference/HOOKS.md | 186 ++ .../payload/reference/PLUGIN-DEVELOPMENT.md | 1436 ++++++++++ .../payload/reference/QUERIES.md | 274 ++ .../references/payload-nextjs-notes.md | 488 ++++ .../website-creator/references/questions.md | 177 ++ .../references/sitemap-template.md | 312 +++ skills/website-creator/scripts/audit-seo.sh | 452 +++ .../website-creator/scripts/convert-astro.sh | 304 +++ skills/website-creator/scripts/deploy.sh | 267 ++ skills/website-creator/scripts/new-project.sh | 343 +++ skills/website-creator/scripts/preview.sh | 188 ++ skills/website-creator/seo-analyzers/SKILL.md | 47 + .../scripts/content_quality_scorer.py | 309 +++ .../seo-analyzers/scripts/requirements.txt | 11 + .../scripts/thai_keyword_analyzer.py | 270 ++ .../seo-analyzers/scripts/thai_readability.py | 334 +++ skills/website-creator/seo-geo/SKILL.md | 115 + .../seo-multi-channel/SKILL.md | 68 + .../seo-multi-channel/scripts/auto_publish.py | 205 ++ .../scripts/generate_content.py | 478 ++++ .../scripts/image_integration.py | 313 +++ .../scripts/requirements.txt | 40 + .../scripts/templates/blog.yaml | 192 ++ .../scripts/templates/facebook.yaml | 82 + .../scripts/templates/facebook_ads.yaml | 121 + .../scripts/templates/google_ads.yaml | 158 ++ .../scripts/templates/x_thread.yaml | 197 ++ .../spec-driven-development/SKILL.md | 200 ++ .../templates/collections/Users.ts | 69 + .../templates/collections/access/index.ts | 44 + .../templates/consent/CookieConsent.astro | 462 ++++ .../templates/consent/README.md | 99 + .../templates/consent/api/consent.ts | 81 + .../consent/api/right-to-be-forgotten.ts | 86 + .../templates/consent/api/route.ts | 80 + .../consent/collections/ConsentLog.ts | 188 ++ .../consent/collections/ConsentLogs.ts | 122 + .../templates/consent/cookie-banner.tsx | 316 +++ .../nextjs-payload-starter/.dockerignore | 9 + .../nextjs-payload-starter/.env.example | 5 + .../nextjs-payload-starter/Dockerfile | 39 + .../nextjs-payload-starter/docker-compose.yml | 40 + .../nextjs-payload-starter/next.config.ts | 31 + .../nextjs-payload-starter/package.json | 45 + .../src/app/(frontend)/globals.css | 41 + .../src/app/(frontend)/layout.tsx | 22 + .../src/app/(frontend)/page.tsx | 84 + .../(payload)/admin/[[...segments]]/page.tsx | 23 + .../src/app/(payload)/admin/importMap.js | 2 + .../app/(payload)/api/[[...slug]]/route.ts | 18 + .../(payload)/api/graphql-playground/route.ts | 6 + .../src/app/(payload)/api/graphql/route.ts | 6 + .../src/app/(payload)/custom.scss | 1 + .../src/app/(payload)/layout.tsx | 30 + .../src/collections/Media.ts | 16 + .../src/collections/Pages.ts | 70 + .../src/collections/Posts.ts | 73 + .../src/collections/Users.ts | 12 + .../nextjs-payload-starter/src/index.ts | 1 + .../src/payload.config.ts | 41 + .../nextjs-payload-starter/tsconfig.json | 28 + .../templates/privacy-policy.md | 104 + .../templates/terms-of-service.md} | 29 + .../ui-styling}/LICENSE.txt | 0 skills/website-creator/ui-styling/SKILL.md | 324 +++ .../ui-styling/canvas-fonts/ArsenalSC-OFL.txt | 93 + .../canvas-fonts/ArsenalSC-Regular.ttf | Bin 0 -> 165848 bytes .../canvas-fonts/BigShoulders-Bold.ttf | Bin 0 -> 94528 bytes .../canvas-fonts/BigShoulders-OFL.txt | 93 + .../canvas-fonts/BigShoulders-Regular.ttf | Bin 0 -> 94396 bytes .../ui-styling/canvas-fonts/Boldonse-OFL.txt | 93 + .../canvas-fonts/Boldonse-Regular.ttf | Bin 0 -> 77168 bytes .../canvas-fonts/BricolageGrotesque-Bold.ttf | Bin 0 -> 90952 bytes .../canvas-fonts/BricolageGrotesque-OFL.txt | 93 + .../BricolageGrotesque-Regular.ttf | Bin 0 -> 90920 bytes .../canvas-fonts/CrimsonPro-Bold.ttf | Bin 0 -> 107352 bytes .../canvas-fonts/CrimsonPro-Italic.ttf | Bin 0 -> 108828 bytes .../canvas-fonts/CrimsonPro-OFL.txt | 93 + .../canvas-fonts/CrimsonPro-Regular.ttf | Bin 0 -> 106696 bytes .../ui-styling/canvas-fonts/DMMono-OFL.txt | 93 + .../canvas-fonts/DMMono-Regular.ttf | Bin 0 -> 48852 bytes .../ui-styling/canvas-fonts/EricaOne-OFL.txt | 94 + .../canvas-fonts/EricaOne-Regular.ttf | Bin 0 -> 24872 bytes .../canvas-fonts/GeistMono-Bold.ttf | Bin 0 -> 78304 bytes .../ui-styling/canvas-fonts/GeistMono-OFL.txt | 93 + .../canvas-fonts/GeistMono-Regular.ttf | Bin 0 -> 78232 bytes .../ui-styling/canvas-fonts/Gloock-OFL.txt | 93 + .../canvas-fonts/Gloock-Regular.ttf | Bin 0 -> 95156 bytes .../canvas-fonts/IBMPlexMono-Bold.ttf | Bin 0 -> 136008 bytes .../canvas-fonts/IBMPlexMono-OFL.txt | 93 + .../canvas-fonts/IBMPlexMono-Regular.ttf | Bin 0 -> 133796 bytes .../canvas-fonts/IBMPlexSerif-Bold.ttf | Bin 0 -> 161000 bytes .../canvas-fonts/IBMPlexSerif-BoldItalic.ttf | Bin 0 -> 169840 bytes .../canvas-fonts/IBMPlexSerif-Italic.ttf | Bin 0 -> 170004 bytes .../canvas-fonts/IBMPlexSerif-Regular.ttf | Bin 0 -> 160380 bytes .../canvas-fonts/InstrumentSans-Bold.ttf | Bin 0 -> 68084 bytes .../InstrumentSans-BoldItalic.ttf | Bin 0 -> 70004 bytes .../canvas-fonts/InstrumentSans-Italic.ttf | Bin 0 -> 69900 bytes .../canvas-fonts/InstrumentSans-OFL.txt | 93 + .../canvas-fonts/InstrumentSans-Regular.ttf | Bin 0 -> 68028 bytes .../canvas-fonts/InstrumentSerif-Italic.ttf | Bin 0 -> 70868 bytes .../canvas-fonts/InstrumentSerif-Regular.ttf | Bin 0 -> 69312 bytes .../ui-styling/canvas-fonts/Italiana-OFL.txt | 93 + .../canvas-fonts/Italiana-Regular.ttf | Bin 0 -> 27184 bytes .../canvas-fonts/JetBrainsMono-Bold.ttf | Bin 0 -> 114828 bytes .../canvas-fonts/JetBrainsMono-OFL.txt | 93 + .../canvas-fonts/JetBrainsMono-Regular.ttf | Bin 0 -> 114904 bytes .../ui-styling/canvas-fonts/Jura-Light.ttf | Bin 0 -> 154308 bytes .../ui-styling/canvas-fonts/Jura-Medium.ttf | Bin 0 -> 154488 bytes .../ui-styling/canvas-fonts/Jura-OFL.txt | 93 + .../canvas-fonts/LibreBaskerville-OFL.txt | 93 + .../canvas-fonts/LibreBaskerville-Regular.ttf | Bin 0 -> 147584 bytes .../ui-styling/canvas-fonts/Lora-Bold.ttf | Bin 0 -> 133828 bytes .../canvas-fonts/Lora-BoldItalic.ttf | Bin 0 -> 140332 bytes .../ui-styling/canvas-fonts/Lora-Italic.ttf | Bin 0 -> 139328 bytes .../ui-styling/canvas-fonts/Lora-OFL.txt | 93 + .../ui-styling/canvas-fonts/Lora-Regular.ttf | Bin 0 -> 133888 bytes .../canvas-fonts/NationalPark-Bold.ttf | Bin 0 -> 79208 bytes .../canvas-fonts/NationalPark-OFL.txt | 93 + .../canvas-fonts/NationalPark-Regular.ttf | Bin 0 -> 76424 bytes .../canvas-fonts/NothingYouCouldDo-OFL.txt | 93 + .../NothingYouCouldDo-Regular.ttf | Bin 0 -> 32020 bytes .../ui-styling/canvas-fonts/Outfit-Bold.ttf | Bin 0 -> 55392 bytes .../ui-styling/canvas-fonts/Outfit-OFL.txt | 93 + .../canvas-fonts/Outfit-Regular.ttf | Bin 0 -> 54912 bytes .../canvas-fonts/PixelifySans-Medium.ttf | Bin 0 -> 51072 bytes .../canvas-fonts/PixelifySans-OFL.txt | 93 + .../ui-styling/canvas-fonts/PoiretOne-OFL.txt | 93 + .../canvas-fonts/PoiretOne-Regular.ttf | Bin 0 -> 45244 bytes .../canvas-fonts/RedHatMono-Bold.ttf | Bin 0 -> 34420 bytes .../canvas-fonts/RedHatMono-OFL.txt | 93 + .../canvas-fonts/RedHatMono-Regular.ttf | Bin 0 -> 34488 bytes .../canvas-fonts/Silkscreen-OFL.txt | 93 + .../canvas-fonts/Silkscreen-Regular.ttf | Bin 0 -> 31960 bytes .../canvas-fonts/SmoochSans-Medium.ttf | Bin 0 -> 59704 bytes .../canvas-fonts/SmoochSans-OFL.txt | 93 + .../ui-styling/canvas-fonts/Tektur-Medium.ttf | Bin 0 -> 76248 bytes .../ui-styling/canvas-fonts/Tektur-OFL.txt | 93 + .../canvas-fonts/Tektur-Regular.ttf | Bin 0 -> 75604 bytes .../ui-styling/canvas-fonts/WorkSans-Bold.ttf | Bin 0 -> 191304 bytes .../canvas-fonts/WorkSans-BoldItalic.ttf | Bin 0 -> 175772 bytes .../canvas-fonts/WorkSans-Italic.ttf | Bin 0 -> 174280 bytes .../ui-styling/canvas-fonts/WorkSans-OFL.txt | 93 + .../canvas-fonts/WorkSans-Regular.ttf | Bin 0 -> 188916 bytes .../canvas-fonts/YoungSerif-OFL.txt | 93 + .../canvas-fonts/YoungSerif-Regular.ttf | Bin 0 -> 105136 bytes .../references/canvas-design-system.md | 320 +++ .../references/shadcn-accessibility.md | 471 ++++ .../references/shadcn-components.md | 424 +++ .../ui-styling/references/shadcn-theming.md | 373 +++ .../references/tailwind-customization.md | 483 ++++ .../references/tailwind-responsive.md | 382 +++ .../references/tailwind-utilities.md | 455 ++++ .../ui-styling/scripts/.coverage | Bin 0 -> 53248 bytes .../ui-styling/scripts/requirements.txt | 17 + .../ui-styling/scripts/shadcn_add.py | 292 ++ .../ui-styling/scripts/tailwind_config_gen.py | 456 ++++ .../ui-styling/scripts/tests/coverage-ui.json | 1 + .../ui-styling/scripts/tests/requirements.txt | 3 + .../scripts/tests/test_shadcn_add.py | 266 ++ .../scripts/tests/test_tailwind_config_gen.py | 336 +++ skills/website-creator/ui-ux-pro-max/SKILL.md | 659 +++++ skills/website-creator/ui-ux-pro-max/data | 1 + skills/website-creator/ui-ux-pro-max/scripts | 1 + 562 files changed, 59030 insertions(+), 37600 deletions(-) create mode 100644 .opencode/package-lock.json delete mode 100644 output/test/results.json delete mode 100644 output/บรการ-podcast-hosting/results.json delete mode 100644 skills/alphaear-deepear-lite/SKILL.md delete mode 100644 skills/alphaear-deepear-lite/scripts/deepear_lite.py delete mode 100644 skills/alphaear-logic-visualizer/SKILL.md delete mode 100644 skills/alphaear-logic-visualizer/references/PROMPTS.md delete mode 100644 skills/alphaear-logic-visualizer/scripts/__init__.py delete mode 100644 skills/alphaear-logic-visualizer/scripts/visualizer.py delete mode 100644 skills/alphaear-logic-visualizer/scripts/visualizer_prompt.py delete mode 100644 skills/alphaear-logic-visualizer/tests/test_visualizer.py delete mode 100644 skills/alphaear-news/SKILL.md delete mode 100644 skills/alphaear-news/references/sources.md delete mode 100644 skills/alphaear-news/scripts/__init__.py delete mode 100644 skills/alphaear-news/scripts/content_extractor.py delete mode 100644 skills/alphaear-news/scripts/database_manager.py delete mode 100644 skills/alphaear-news/scripts/news_tools.py delete mode 100644 skills/alphaear-news/tests/test_news.py delete mode 100644 skills/alphaear-predictor/SKILL.md delete mode 100644 skills/alphaear-predictor/references/PROMPTS.md delete mode 100644 skills/alphaear-predictor/scripts/__init__.py delete mode 100644 skills/alphaear-predictor/scripts/forecast_agent.py delete mode 100644 skills/alphaear-predictor/scripts/json_utils.py delete mode 100644 skills/alphaear-predictor/scripts/kronos_predictor.py delete mode 100644 skills/alphaear-predictor/scripts/predictor/exports/models/kronos_news_v1_20260101_0015.pt delete mode 100644 skills/alphaear-predictor/scripts/predictor/model/__init__.py delete mode 100644 skills/alphaear-predictor/scripts/predictor/model/kronos.py delete mode 100644 skills/alphaear-predictor/scripts/predictor/model/module.py delete mode 100644 skills/alphaear-predictor/scripts/prompts/fin_agent.py delete mode 100644 skills/alphaear-predictor/scripts/prompts/forecast_analyst.py delete mode 100644 skills/alphaear-predictor/scripts/prompts/intent_agent.py delete mode 100644 skills/alphaear-predictor/scripts/prompts/isq_prompt_generator.py delete mode 100644 skills/alphaear-predictor/scripts/prompts/report_agent.py delete mode 100644 skills/alphaear-predictor/scripts/prompts/trend_agent.py delete mode 100644 skills/alphaear-predictor/scripts/prompts/visualizer.py delete mode 100644 skills/alphaear-predictor/scripts/schema/isq_template.py delete mode 100644 skills/alphaear-predictor/scripts/schema/models.py delete mode 100644 skills/alphaear-predictor/scripts/utils/__init__.py delete mode 100644 skills/alphaear-predictor/scripts/utils/database_manager.py delete mode 100644 skills/alphaear-predictor/scripts/utils/json_utils.py delete mode 100644 skills/alphaear-predictor/scripts/utils/llm/capability.py delete mode 100644 skills/alphaear-predictor/scripts/utils/llm/factory.py delete mode 100644 skills/alphaear-predictor/scripts/utils/llm/router.py delete mode 100644 skills/alphaear-predictor/scripts/utils/logging_setup.py delete mode 100644 skills/alphaear-predictor/scripts/utils/predictor/evaluation.py delete mode 100644 skills/alphaear-predictor/scripts/utils/predictor/kline_generate.py delete mode 100644 skills/alphaear-predictor/scripts/utils/predictor/model/__init__.py delete mode 100644 skills/alphaear-predictor/scripts/utils/predictor/model/kronos.py delete mode 100644 skills/alphaear-predictor/scripts/utils/predictor/model/module.py delete mode 100644 skills/alphaear-predictor/scripts/utils/predictor/training.py delete mode 100644 skills/alphaear-predictor/scripts/utils/search_tools.py delete mode 100644 skills/alphaear-predictor/scripts/utils/stock_tools.py delete mode 100644 skills/alphaear-predictor/tests/test_predictor.py delete mode 100644 skills/alphaear-reporter/SKILL.md delete mode 100644 skills/alphaear-reporter/references/PROMPTS.md delete mode 100644 skills/alphaear-reporter/scripts/__init__.py delete mode 100644 skills/alphaear-reporter/scripts/prompts/fin_agent.py delete mode 100644 skills/alphaear-reporter/scripts/prompts/forecast_analyst.py delete mode 100644 skills/alphaear-reporter/scripts/prompts/intent_agent.py delete mode 100644 skills/alphaear-reporter/scripts/prompts/isq_prompt_generator.py delete mode 100644 skills/alphaear-reporter/scripts/prompts/report_agent.py delete mode 100644 skills/alphaear-reporter/scripts/prompts/trend_agent.py delete mode 100644 skills/alphaear-reporter/scripts/prompts/visualizer.py delete mode 100644 skills/alphaear-reporter/scripts/report_agent.py delete mode 100644 skills/alphaear-reporter/scripts/schema/isq_template.py delete mode 100644 skills/alphaear-reporter/scripts/schema/models.py delete mode 100644 skills/alphaear-reporter/scripts/tools/__init__.py delete mode 100644 skills/alphaear-reporter/scripts/tools/toolkits.py delete mode 100644 skills/alphaear-reporter/scripts/utils/__init__.py delete mode 100644 skills/alphaear-reporter/scripts/utils/content_extractor.py delete mode 100644 skills/alphaear-reporter/scripts/utils/database_manager.py delete mode 100644 skills/alphaear-reporter/scripts/utils/hybrid_search.py delete mode 100644 skills/alphaear-reporter/scripts/utils/json_utils.py delete mode 100644 skills/alphaear-reporter/scripts/utils/llm/capability.py delete mode 100644 skills/alphaear-reporter/scripts/utils/llm/factory.py delete mode 100644 skills/alphaear-reporter/scripts/utils/llm/router.py delete mode 100644 skills/alphaear-reporter/scripts/utils/logging_setup.py delete mode 100644 skills/alphaear-reporter/scripts/utils/news_tools.py delete mode 100644 skills/alphaear-reporter/scripts/utils/predictor/evaluation.py delete mode 100644 skills/alphaear-reporter/scripts/utils/predictor/kline_generate.py delete mode 100644 skills/alphaear-reporter/scripts/utils/predictor/model/__init__.py delete mode 100644 skills/alphaear-reporter/scripts/utils/predictor/model/kronos.py delete mode 100644 skills/alphaear-reporter/scripts/utils/predictor/model/module.py delete mode 100644 skills/alphaear-reporter/scripts/utils/predictor/training.py delete mode 100644 skills/alphaear-reporter/scripts/utils/search_tools.py delete mode 100644 skills/alphaear-reporter/scripts/utils/sentiment_tools.py delete mode 100644 skills/alphaear-reporter/scripts/utils/stock_tools.py delete mode 100644 skills/alphaear-reporter/scripts/visualizer.py delete mode 100644 skills/alphaear-reporter/tests/test_reporter.py delete mode 100644 skills/alphaear-search/SKILL.md delete mode 100644 skills/alphaear-search/references/PROMPTS.md delete mode 100644 skills/alphaear-search/scripts/__init__.py delete mode 100644 skills/alphaear-search/scripts/content_extractor.py delete mode 100644 skills/alphaear-search/scripts/database_manager.py delete mode 100644 skills/alphaear-search/scripts/hybrid_search.py delete mode 100644 skills/alphaear-search/scripts/llm/__init__.py delete mode 100644 skills/alphaear-search/scripts/llm/capability.py delete mode 100644 skills/alphaear-search/scripts/llm/factory.py delete mode 100644 skills/alphaear-search/scripts/llm/router.py delete mode 100644 skills/alphaear-search/scripts/search_tools.py delete mode 100644 skills/alphaear-search/scripts/sentiment_tools.py delete mode 100644 skills/alphaear-search/tests/test_search.py delete mode 100644 skills/alphaear-sentiment/SKILL.md delete mode 100644 skills/alphaear-sentiment/scripts/__init__.py delete mode 100644 skills/alphaear-sentiment/scripts/database_manager.py delete mode 100644 skills/alphaear-sentiment/scripts/llm/capability.py delete mode 100644 skills/alphaear-sentiment/scripts/llm/factory.py delete mode 100644 skills/alphaear-sentiment/scripts/llm/router.py delete mode 100644 skills/alphaear-sentiment/scripts/sentiment_tools.py delete mode 100644 skills/alphaear-sentiment/tests/test_sentiment.py delete mode 100644 skills/alphaear-signal-tracker/SKILL.md delete mode 100644 skills/alphaear-signal-tracker/references/PROMPTS.md delete mode 100644 skills/alphaear-signal-tracker/scripts/__init__.py delete mode 100644 skills/alphaear-signal-tracker/scripts/fin_agent.py delete mode 100644 skills/alphaear-signal-tracker/scripts/prompts/fin_agent.py delete mode 100644 skills/alphaear-signal-tracker/scripts/prompts/forecast_analyst.py delete mode 100644 skills/alphaear-signal-tracker/scripts/prompts/intent_agent.py delete mode 100644 skills/alphaear-signal-tracker/scripts/prompts/isq_prompt_generator.py delete mode 100644 skills/alphaear-signal-tracker/scripts/prompts/report_agent.py delete mode 100644 skills/alphaear-signal-tracker/scripts/prompts/trend_agent.py delete mode 100644 skills/alphaear-signal-tracker/scripts/prompts/visualizer.py delete mode 100644 skills/alphaear-signal-tracker/scripts/schema/isq_template.py delete mode 100644 skills/alphaear-signal-tracker/scripts/schema/models.py delete mode 100644 skills/alphaear-signal-tracker/scripts/tools/__init__.py delete mode 100644 skills/alphaear-signal-tracker/scripts/tools/toolkits.py delete mode 100644 skills/alphaear-signal-tracker/scripts/utils/__init__.py delete mode 100644 skills/alphaear-signal-tracker/scripts/utils/content_extractor.py delete mode 100644 skills/alphaear-signal-tracker/scripts/utils/database_manager.py delete mode 100644 skills/alphaear-signal-tracker/scripts/utils/hybrid_search.py delete mode 100644 skills/alphaear-signal-tracker/scripts/utils/json_utils.py delete mode 100644 skills/alphaear-signal-tracker/scripts/utils/llm/capability.py delete mode 100644 skills/alphaear-signal-tracker/scripts/utils/llm/factory.py delete mode 100644 skills/alphaear-signal-tracker/scripts/utils/llm/router.py delete mode 100644 skills/alphaear-signal-tracker/scripts/utils/logging_setup.py delete mode 100644 skills/alphaear-signal-tracker/scripts/utils/md_to_html.py delete mode 100644 skills/alphaear-signal-tracker/scripts/utils/news_tools.py delete mode 100644 skills/alphaear-signal-tracker/scripts/utils/predictor/evaluation.py delete mode 100644 skills/alphaear-signal-tracker/scripts/utils/predictor/kline_generate.py delete mode 100644 skills/alphaear-signal-tracker/scripts/utils/predictor/model/__init__.py delete mode 100644 skills/alphaear-signal-tracker/scripts/utils/predictor/model/kronos.py delete mode 100644 skills/alphaear-signal-tracker/scripts/utils/predictor/model/module.py delete mode 100644 skills/alphaear-signal-tracker/scripts/utils/predictor/training.py delete mode 100644 skills/alphaear-signal-tracker/scripts/utils/search_tools.py delete mode 100644 skills/alphaear-signal-tracker/scripts/utils/sentiment_tools.py delete mode 100644 skills/alphaear-signal-tracker/scripts/utils/stock_tools.py delete mode 100644 skills/alphaear-signal-tracker/tests/test_tracker.py delete mode 100644 skills/alphaear-stock/SKILL.md delete mode 100644 skills/alphaear-stock/scripts/__init__.py delete mode 100644 skills/alphaear-stock/scripts/database_manager.py delete mode 100644 skills/alphaear-stock/scripts/stock_tools.py delete mode 100644 skills/alphaear-stock/tests/test_stock.py create mode 100644 skills/ecommerce-astro/SKILL.md create mode 100644 skills/ecommerce-astro/scripts/.env.example create mode 100755 skills/ecommerce-astro/scripts/create_ecommerce.py create mode 100644 skills/ecommerce-astro/scripts/requirements.txt create mode 100644 skills/ecommerce-astro/scripts/supabase_migration.sql create mode 100644 skills/ecommerce-astro/scripts/templates/Dockerfile create mode 100644 skills/ecommerce-astro/scripts/templates/docker-compose.yml create mode 100644 skills/ecommerce-astro/scripts/templates/src/components/admin/AdminSidebar.astro create mode 100644 skills/ecommerce-astro/scripts/templates/src/components/admin/AdminSidebar.tsx create mode 100644 skills/ecommerce-astro/scripts/templates/src/components/admin/DataTable.tsx create mode 100644 skills/ecommerce-astro/scripts/templates/src/components/admin/OrderStatusManager.tsx create mode 100644 skills/ecommerce-astro/scripts/templates/src/components/cart/AddToCart.tsx create mode 100644 skills/ecommerce-astro/scripts/templates/src/components/cart/CartBadge.tsx create mode 100644 skills/ecommerce-astro/scripts/templates/src/components/cart/CartDrawer.tsx create mode 100644 skills/ecommerce-astro/scripts/templates/src/components/cart/CartItems.tsx create mode 100644 skills/ecommerce-astro/scripts/templates/src/components/cart/CartSummary.tsx create mode 100644 skills/ecommerce-astro/scripts/templates/src/components/checkout/BillingInfo.tsx create mode 100644 skills/ecommerce-astro/scripts/templates/src/components/checkout/CheckoutForm.tsx create mode 100644 skills/ecommerce-astro/scripts/templates/src/components/checkout/CheckoutSingleItem.tsx create mode 100644 skills/ecommerce-astro/scripts/templates/src/components/checkout/CheckoutSingleItemDark.tsx create mode 100644 skills/ecommerce-astro/scripts/templates/src/components/checkout/OrderStatusManager.tsx create mode 100644 skills/ecommerce-astro/scripts/templates/src/components/checkout/OrderSummary.tsx create mode 100644 skills/ecommerce-astro/scripts/templates/src/components/checkout/PaymentDetails.tsx create mode 100644 skills/ecommerce-astro/scripts/templates/src/components/checkout/PaymentMethod.tsx create mode 100644 skills/ecommerce-astro/scripts/templates/src/components/checkout/ShippingForm.tsx create mode 100644 skills/ecommerce-astro/scripts/templates/src/components/incentives/IncentiveCols.tsx create mode 100644 skills/ecommerce-astro/scripts/templates/src/components/incentives/IncentiveLarge.tsx create mode 100644 skills/ecommerce-astro/scripts/templates/src/components/layout/Footer.astro create mode 100644 skills/ecommerce-astro/scripts/templates/src/components/layout/Header.astro create mode 100644 skills/ecommerce-astro/scripts/templates/src/components/order/OrderCardProduct.tsx create mode 100644 skills/ecommerce-astro/scripts/templates/src/components/order/OrderHistory.tsx create mode 100644 skills/ecommerce-astro/scripts/templates/src/components/order/OrderHistoryCard.tsx create mode 100644 skills/ecommerce-astro/scripts/templates/src/components/order/OrderProductRow.tsx create mode 100644 skills/ecommerce-astro/scripts/templates/src/components/order/OrderSummaries.tsx create mode 100644 skills/ecommerce-astro/scripts/templates/src/components/product/CardCategory.tsx create mode 100644 skills/ecommerce-astro/scripts/templates/src/components/product/ProductAccordion.tsx create mode 100644 skills/ecommerce-astro/scripts/templates/src/components/product/ProductBadge.tsx create mode 100644 skills/ecommerce-astro/scripts/templates/src/components/product/ProductCard.astro create mode 100644 skills/ecommerce-astro/scripts/templates/src/components/product/ProductFeature.tsx create mode 100644 skills/ecommerce-astro/scripts/templates/src/components/product/ProductFeature2.tsx create mode 100644 skills/ecommerce-astro/scripts/templates/src/components/product/ProductFilters.tsx create mode 100644 skills/ecommerce-astro/scripts/templates/src/components/product/ProductGallery.tsx create mode 100644 skills/ecommerce-astro/scripts/templates/src/components/product/ProductOverviewGallery.tsx create mode 100644 skills/ecommerce-astro/scripts/templates/src/components/product/ProductOverviewGrid.tsx create mode 100644 skills/ecommerce-astro/scripts/templates/src/components/product/ProductQuickview2.tsx create mode 100644 skills/ecommerce-astro/scripts/templates/src/components/product/ProductRating.tsx create mode 100644 skills/ecommerce-astro/scripts/templates/src/components/product/ProductSizes.tsx create mode 100644 skills/ecommerce-astro/scripts/templates/src/components/product/ProductVariants.tsx create mode 100644 skills/ecommerce-astro/scripts/templates/src/components/product/StockBadge.tsx create mode 100644 skills/ecommerce-astro/scripts/templates/src/components/product/WishlistButton.tsx create mode 100644 skills/ecommerce-astro/scripts/templates/src/components/promo/PromoSectionLarge.tsx create mode 100644 skills/ecommerce-astro/scripts/templates/src/components/promo/TestimonialsFade.tsx create mode 100644 skills/ecommerce-astro/scripts/templates/src/components/review/ReviewComment.tsx create mode 100644 skills/ecommerce-astro/scripts/templates/src/components/review/ReviewForm.tsx create mode 100644 skills/ecommerce-astro/scripts/templates/src/components/review/ReviewList.tsx create mode 100644 skills/ecommerce-astro/scripts/templates/src/components/review/ReviewProgress.tsx create mode 100644 skills/ecommerce-astro/scripts/templates/src/components/review/StarRating.tsx create mode 100644 skills/ecommerce-astro/scripts/templates/src/components/store/StoreDoubleColumn.tsx create mode 100644 skills/ecommerce-astro/scripts/templates/src/components/store/StoreNavbar.tsx create mode 100644 skills/ecommerce-astro/scripts/templates/src/components/store/StoreNavigation.tsx create mode 100644 skills/ecommerce-astro/scripts/templates/src/components/store/UpperNavbar.tsx create mode 100644 skills/ecommerce-astro/scripts/templates/src/components/ui/Button.tsx create mode 100644 skills/ecommerce-astro/scripts/templates/src/components/ui/Input.tsx create mode 100644 skills/ecommerce-astro/scripts/templates/src/components/ui/Modal.tsx create mode 100644 skills/ecommerce-astro/scripts/templates/src/components/ui/Select.tsx create mode 100644 skills/ecommerce-astro/scripts/templates/src/components/vendor/EarningsChart.tsx create mode 100644 skills/ecommerce-astro/scripts/templates/src/components/vendor/ProductForm.tsx create mode 100644 skills/ecommerce-astro/scripts/templates/src/components/vendor/VendorCard.tsx create mode 100644 skills/ecommerce-astro/scripts/templates/src/i18n/en.json create mode 100644 skills/ecommerce-astro/scripts/templates/src/i18n/th.json create mode 100644 skills/ecommerce-astro/scripts/templates/src/layouts/Layout.astro create mode 100644 skills/ecommerce-astro/scripts/templates/src/lib/auth.ts create mode 100644 skills/ecommerce-astro/scripts/templates/src/lib/db.ts create mode 100644 skills/ecommerce-astro/scripts/templates/src/lib/payso.ts create mode 100644 skills/ecommerce-astro/scripts/templates/src/lib/stripe.ts create mode 100644 skills/ecommerce-astro/scripts/templates/src/lib/supabase.ts create mode 100644 skills/ecommerce-astro/scripts/templates/src/lib/utils.ts create mode 100644 skills/ecommerce-astro/scripts/templates/src/pages/account/index.astro create mode 100644 skills/ecommerce-astro/scripts/templates/src/pages/account/orders/[id].astro create mode 100644 skills/ecommerce-astro/scripts/templates/src/pages/account/orders/index.astro create mode 100644 skills/ecommerce-astro/scripts/templates/src/pages/admin/categories.astro create mode 100644 skills/ecommerce-astro/scripts/templates/src/pages/admin/dashboard.astro create mode 100644 skills/ecommerce-astro/scripts/templates/src/pages/admin/orders.astro create mode 100644 skills/ecommerce-astro/scripts/templates/src/pages/admin/users.astro create mode 100644 skills/ecommerce-astro/scripts/templates/src/pages/admin/vendors.astro create mode 100644 skills/ecommerce-astro/scripts/templates/src/pages/api/auth/login.ts create mode 100644 skills/ecommerce-astro/scripts/templates/src/pages/api/auth/logout.ts create mode 100644 skills/ecommerce-astro/scripts/templates/src/pages/api/auth/me.ts create mode 100644 skills/ecommerce-astro/scripts/templates/src/pages/api/auth/register.ts create mode 100644 skills/ecommerce-astro/scripts/templates/src/pages/api/categories/index.ts create mode 100644 skills/ecommerce-astro/scripts/templates/src/pages/api/orders/[id].ts create mode 100644 skills/ecommerce-astro/scripts/templates/src/pages/api/orders/index.ts create mode 100644 skills/ecommerce-astro/scripts/templates/src/pages/api/payments/callback.ts create mode 100644 skills/ecommerce-astro/scripts/templates/src/pages/api/payments/create.ts create mode 100644 skills/ecommerce-astro/scripts/templates/src/pages/api/payments/inquiry.ts create mode 100644 skills/ecommerce-astro/scripts/templates/src/pages/api/products/[slug].ts create mode 100644 skills/ecommerce-astro/scripts/templates/src/pages/api/products/index.ts create mode 100644 skills/ecommerce-astro/scripts/templates/src/pages/api/vendors/[slug].ts create mode 100644 skills/ecommerce-astro/scripts/templates/src/pages/api/vendors/apply.ts create mode 100644 skills/ecommerce-astro/scripts/templates/src/pages/api/vendors/dashboard.ts create mode 100644 skills/ecommerce-astro/scripts/templates/src/pages/api/vendors/index.ts create mode 100644 skills/ecommerce-astro/scripts/templates/src/pages/api/webhooks/payso.ts create mode 100644 skills/ecommerce-astro/scripts/templates/src/pages/cart.astro create mode 100644 skills/ecommerce-astro/scripts/templates/src/pages/checkout.astro create mode 100644 skills/ecommerce-astro/scripts/templates/src/pages/checkout/success.astro create mode 100644 skills/ecommerce-astro/scripts/templates/src/pages/index.astro create mode 100644 skills/ecommerce-astro/scripts/templates/src/pages/login.astro create mode 100644 skills/ecommerce-astro/scripts/templates/src/pages/products/[slug].astro create mode 100644 skills/ecommerce-astro/scripts/templates/src/pages/products/index.astro create mode 100644 skills/ecommerce-astro/scripts/templates/src/pages/register.astro create mode 100644 skills/ecommerce-astro/scripts/templates/src/pages/vendor/dashboard.astro create mode 100644 skills/ecommerce-astro/scripts/templates/src/pages/vendor/orders.astro create mode 100644 skills/ecommerce-astro/scripts/templates/src/pages/vendor/products/[id]/edit.astro create mode 100644 skills/ecommerce-astro/scripts/templates/src/pages/vendor/products/index.astro create mode 100644 skills/ecommerce-astro/scripts/templates/src/pages/vendor/products/new.astro create mode 100644 skills/ecommerce-astro/scripts/templates/src/pages/vendor/settings.astro create mode 100644 skills/ecommerce-astro/scripts/templates/src/pages/vendors/[slug].astro create mode 100644 skills/ecommerce-astro/scripts/templates/src/pages/vendors/index.astro create mode 100644 skills/ecommerce-astro/scripts/templates/src/pages/wishlist.astro create mode 100644 skills/ecommerce-astro/scripts/templates/src/stores/auth.ts create mode 100644 skills/ecommerce-astro/scripts/templates/src/stores/cart.ts create mode 100644 skills/ecommerce-astro/scripts/templates/src/stores/vendor.ts create mode 100644 skills/mql-developer/LICENSE create mode 100644 skills/mql-developer/README.md create mode 100644 skills/mql-developer/SKILL.md create mode 100644 skills/mql-developer/fibo-zone-indicator/SKILL.md create mode 100644 skills/mql-developer/mql5-dashboard-survive-tf-change/SKILL.md create mode 100644 skills/mql-developer/mql5-ea-troubleshooting/SKILL.md create mode 100644 skills/mql-developer/references/architecture-patterns.md create mode 100644 skills/mql-developer/references/backtesting.md create mode 100644 skills/mql-developer/references/external-communication.md create mode 100644 skills/mql-developer/references/indicators-and-ui.md create mode 100644 skills/mql-developer/references/mql4-reference.md create mode 100644 skills/mql-developer/references/mql5-reference.md create mode 100644 skills/mql-developer/references/security-licensing.md create mode 100644 skills/mql-developer/references/trading-operations.md delete mode 100644 skills/shodh-memory/SKILL.md delete mode 100644 skills/shodh-memory/scripts/.env.example delete mode 100755 skills/shodh-memory/scripts/cli.py delete mode 100755 skills/shodh-memory/scripts/install.sh delete mode 100644 skills/shodh-memory/scripts/requirements.txt delete mode 100644 skills/thai-frontend-dev/AUTO_ADMIN_PASSWORD.md delete mode 100644 skills/thai-frontend-dev/AUTO_DEPLOY_COMPLETE.md delete mode 100644 skills/thai-frontend-dev/AUTO_DEPLOY_IMPLEMENTATION.md delete mode 100644 skills/thai-frontend-dev/AUTO_DEPLOY_PROGRESS.md delete mode 100644 skills/thai-frontend-dev/EASYPANEL_INTEGRATION.md delete mode 100644 skills/thai-frontend-dev/FINAL_SUMMARY.md delete mode 100644 skills/thai-frontend-dev/IMPLEMENTATION_STATUS.md delete mode 100644 skills/thai-frontend-dev/IMPLEMENTATION_SUMMARY.md delete mode 100644 skills/thai-frontend-dev/MIGRATION_WORKFLOW.md delete mode 100644 skills/thai-frontend-dev/README.md delete mode 100644 skills/thai-frontend-dev/SKILL.md delete mode 100644 skills/thai-frontend-dev/SPECIFICATION.md delete mode 100644 skills/thai-frontend-dev/TEST_REPORT.md delete mode 100644 skills/thai-frontend-dev/UPDATE_SUMMARY.md delete mode 100644 skills/thai-frontend-dev/scripts/.env.example delete mode 100644 skills/thai-frontend-dev/scripts/create_astro_website.py delete mode 100644 skills/thai-frontend-dev/scripts/migrate_existing_website.py delete mode 100644 skills/thai-frontend-dev/scripts/refactor_existing_website.py delete mode 100644 skills/thai-frontend-dev/scripts/requirements.txt delete mode 100644 skills/thai-frontend-dev/scripts/templates/admin-consent-logs.astro delete mode 100644 skills/thai-frontend-dev/scripts/templates/components/common/Footer.astro delete mode 100644 skills/thai-frontend-dev/scripts/templates/components/common/Header.astro delete mode 100644 skills/thai-frontend-dev/scripts/templates/icons/line.svg delete mode 100644 skills/thai-frontend-dev/scripts/templates/layouts/BaseLayout.astro delete mode 100644 skills/thai-frontend-dev/scripts/templates/pages/index.astro delete mode 100644 skills/thai-frontend-dev/scripts/templates/styles/global.css delete mode 100644 skills/thai-frontend-dev/scripts/templates/thai-privacy-policy-template.md delete mode 100644 skills/thai-frontend-dev/scripts/umami_integration.py delete mode 100644 skills/theme-factory/SKILL.md delete mode 100644 skills/theme-factory/theme-showcase.pdf delete mode 100644 skills/theme-factory/themes/arctic-frost.md delete mode 100644 skills/theme-factory/themes/botanical-garden.md delete mode 100644 skills/theme-factory/themes/desert-rose.md delete mode 100644 skills/theme-factory/themes/forest-canopy.md delete mode 100644 skills/theme-factory/themes/golden-hour.md delete mode 100644 skills/theme-factory/themes/midnight-galaxy.md delete mode 100644 skills/theme-factory/themes/modern-minimalist.md delete mode 100644 skills/theme-factory/themes/ocean-depths.md delete mode 100644 skills/theme-factory/themes/sunset-boulevard.md delete mode 100644 skills/theme-factory/themes/tech-innovation.md create mode 100644 skills/website-creator/.DS_Store create mode 100644 skills/website-creator/SKILL.md create mode 100644 skills/website-creator/api-and-interface-design/SKILL.md create mode 100644 skills/website-creator/creative/.DS_Store create mode 100644 skills/website-creator/creative/picture-it/SKILL.md create mode 100644 skills/website-creator/creative/picture-it/references/pipeline-templates.json create mode 100644 skills/website-creator/creative/picture-it/scripts/thai-font-patch.ts create mode 100644 skills/website-creator/design/SKILL.md create mode 100644 skills/website-creator/design/data/cip/deliverables.csv create mode 100644 skills/website-creator/design/data/cip/industries.csv create mode 100644 skills/website-creator/design/data/cip/mockup-contexts.csv create mode 100644 skills/website-creator/design/data/cip/styles.csv create mode 100644 skills/website-creator/design/data/icon/styles.csv create mode 100644 skills/website-creator/design/data/logo/colors.csv create mode 100644 skills/website-creator/design/data/logo/industries.csv create mode 100644 skills/website-creator/design/data/logo/styles.csv create mode 100644 skills/website-creator/design/references/banner-sizes-and-styles.md create mode 100644 skills/website-creator/design/references/cip-deliverable-guide.md create mode 100644 skills/website-creator/design/references/cip-design.md create mode 100644 skills/website-creator/design/references/cip-prompt-engineering.md create mode 100644 skills/website-creator/design/references/cip-style-guide.md create mode 100644 skills/website-creator/design/references/design-routing.md create mode 100644 skills/website-creator/design/references/icon-design.md create mode 100644 skills/website-creator/design/references/logo-color-psychology.md create mode 100644 skills/website-creator/design/references/logo-design.md create mode 100644 skills/website-creator/design/references/logo-prompt-engineering.md create mode 100644 skills/website-creator/design/references/logo-style-guide.md create mode 100644 skills/website-creator/design/references/slides-copywriting-formulas.md create mode 100644 skills/website-creator/design/references/slides-create.md create mode 100644 skills/website-creator/design/references/slides-html-template.md create mode 100644 skills/website-creator/design/references/slides-layout-patterns.md create mode 100644 skills/website-creator/design/references/slides-strategies.md create mode 100644 skills/website-creator/design/references/slides.md create mode 100644 skills/website-creator/design/references/social-photos-design.md create mode 100644 skills/website-creator/design/scripts/cip/core.py create mode 100644 skills/website-creator/design/scripts/cip/generate.py create mode 100644 skills/website-creator/design/scripts/cip/render-html.py create mode 100644 skills/website-creator/design/scripts/cip/search.py create mode 100644 skills/website-creator/design/scripts/icon/generate.py create mode 100644 skills/website-creator/design/scripts/logo/core.py create mode 100644 skills/website-creator/design/scripts/logo/generate.py create mode 100644 skills/website-creator/design/scripts/logo/search.py create mode 100644 skills/website-creator/frontend-ui-engineering/SKILL.md create mode 100644 skills/website-creator/general/plan/SKILL.md create mode 100644 skills/website-creator/general/requesting-code-review/SKILL.md create mode 100644 skills/website-creator/general/skill-augmentation-from-source/SKILL.md create mode 100644 skills/website-creator/general/subagent-driven-development/SKILL.md create mode 100644 skills/website-creator/general/systematic-debugging/SKILL.md create mode 100644 skills/website-creator/general/test-driven-development/SKILL.md create mode 100644 skills/website-creator/general/writing-plans/SKILL.md create mode 100644 skills/website-creator/payload-lexical-integration/SKILL.md create mode 100644 skills/website-creator/payload-nextjs-turbopack-fix/SKILL.md create mode 100644 skills/website-creator/payload-v3-admin-init/SKILL.md create mode 100644 skills/website-creator/payload/SKILL.md create mode 100644 skills/website-creator/payload/reference/ACCESS-CONTROL-ADVANCED.md create mode 100644 skills/website-creator/payload/reference/ACCESS-CONTROL.md create mode 100644 skills/website-creator/payload/reference/ADAPTERS.md create mode 100644 skills/website-creator/payload/reference/ADVANCED.md create mode 100644 skills/website-creator/payload/reference/COLLECTIONS.md create mode 100644 skills/website-creator/payload/reference/ENDPOINTS.md create mode 100644 skills/website-creator/payload/reference/FIELD-TYPE-GUARDS.md create mode 100644 skills/website-creator/payload/reference/FIELDS.md create mode 100644 skills/website-creator/payload/reference/HOOKS.md create mode 100644 skills/website-creator/payload/reference/PLUGIN-DEVELOPMENT.md create mode 100644 skills/website-creator/payload/reference/QUERIES.md create mode 100644 skills/website-creator/references/payload-nextjs-notes.md create mode 100644 skills/website-creator/references/questions.md create mode 100644 skills/website-creator/references/sitemap-template.md create mode 100755 skills/website-creator/scripts/audit-seo.sh create mode 100755 skills/website-creator/scripts/convert-astro.sh create mode 100755 skills/website-creator/scripts/deploy.sh create mode 100755 skills/website-creator/scripts/new-project.sh create mode 100755 skills/website-creator/scripts/preview.sh create mode 100644 skills/website-creator/seo-analyzers/SKILL.md create mode 100644 skills/website-creator/seo-analyzers/scripts/content_quality_scorer.py create mode 100644 skills/website-creator/seo-analyzers/scripts/requirements.txt create mode 100644 skills/website-creator/seo-analyzers/scripts/thai_keyword_analyzer.py create mode 100644 skills/website-creator/seo-analyzers/scripts/thai_readability.py create mode 100644 skills/website-creator/seo-geo/SKILL.md create mode 100644 skills/website-creator/seo-multi-channel/SKILL.md create mode 100644 skills/website-creator/seo-multi-channel/scripts/auto_publish.py create mode 100644 skills/website-creator/seo-multi-channel/scripts/generate_content.py create mode 100644 skills/website-creator/seo-multi-channel/scripts/image_integration.py create mode 100644 skills/website-creator/seo-multi-channel/scripts/requirements.txt create mode 100644 skills/website-creator/seo-multi-channel/scripts/templates/blog.yaml create mode 100644 skills/website-creator/seo-multi-channel/scripts/templates/facebook.yaml create mode 100644 skills/website-creator/seo-multi-channel/scripts/templates/facebook_ads.yaml create mode 100644 skills/website-creator/seo-multi-channel/scripts/templates/google_ads.yaml create mode 100644 skills/website-creator/seo-multi-channel/scripts/templates/x_thread.yaml create mode 100644 skills/website-creator/spec-driven-development/SKILL.md create mode 100644 skills/website-creator/templates/collections/Users.ts create mode 100644 skills/website-creator/templates/collections/access/index.ts create mode 100644 skills/website-creator/templates/consent/CookieConsent.astro create mode 100644 skills/website-creator/templates/consent/README.md create mode 100644 skills/website-creator/templates/consent/api/consent.ts create mode 100644 skills/website-creator/templates/consent/api/right-to-be-forgotten.ts create mode 100644 skills/website-creator/templates/consent/api/route.ts create mode 100644 skills/website-creator/templates/consent/collections/ConsentLog.ts create mode 100644 skills/website-creator/templates/consent/collections/ConsentLogs.ts create mode 100644 skills/website-creator/templates/consent/cookie-banner.tsx create mode 100644 skills/website-creator/templates/nextjs-payload-starter/.dockerignore create mode 100644 skills/website-creator/templates/nextjs-payload-starter/.env.example create mode 100644 skills/website-creator/templates/nextjs-payload-starter/Dockerfile create mode 100644 skills/website-creator/templates/nextjs-payload-starter/docker-compose.yml create mode 100644 skills/website-creator/templates/nextjs-payload-starter/next.config.ts create mode 100644 skills/website-creator/templates/nextjs-payload-starter/package.json create mode 100644 skills/website-creator/templates/nextjs-payload-starter/src/app/(frontend)/globals.css create mode 100644 skills/website-creator/templates/nextjs-payload-starter/src/app/(frontend)/layout.tsx create mode 100644 skills/website-creator/templates/nextjs-payload-starter/src/app/(frontend)/page.tsx create mode 100644 skills/website-creator/templates/nextjs-payload-starter/src/app/(payload)/admin/[[...segments]]/page.tsx create mode 100644 skills/website-creator/templates/nextjs-payload-starter/src/app/(payload)/admin/importMap.js create mode 100644 skills/website-creator/templates/nextjs-payload-starter/src/app/(payload)/api/[[...slug]]/route.ts create mode 100644 skills/website-creator/templates/nextjs-payload-starter/src/app/(payload)/api/graphql-playground/route.ts create mode 100644 skills/website-creator/templates/nextjs-payload-starter/src/app/(payload)/api/graphql/route.ts create mode 100644 skills/website-creator/templates/nextjs-payload-starter/src/app/(payload)/custom.scss create mode 100644 skills/website-creator/templates/nextjs-payload-starter/src/app/(payload)/layout.tsx create mode 100644 skills/website-creator/templates/nextjs-payload-starter/src/collections/Media.ts create mode 100644 skills/website-creator/templates/nextjs-payload-starter/src/collections/Pages.ts create mode 100644 skills/website-creator/templates/nextjs-payload-starter/src/collections/Posts.ts create mode 100644 skills/website-creator/templates/nextjs-payload-starter/src/collections/Users.ts create mode 100644 skills/website-creator/templates/nextjs-payload-starter/src/index.ts create mode 100644 skills/website-creator/templates/nextjs-payload-starter/src/payload.config.ts create mode 100644 skills/website-creator/templates/nextjs-payload-starter/tsconfig.json create mode 100644 skills/website-creator/templates/privacy-policy.md rename skills/{thai-frontend-dev/scripts/templates/thai-terms-of-service-template.md => website-creator/templates/terms-of-service.md} (92%) rename skills/{theme-factory => website-creator/ui-styling}/LICENSE.txt (100%) create mode 100644 skills/website-creator/ui-styling/SKILL.md create mode 100644 skills/website-creator/ui-styling/canvas-fonts/ArsenalSC-OFL.txt create mode 100644 skills/website-creator/ui-styling/canvas-fonts/ArsenalSC-Regular.ttf create mode 100644 skills/website-creator/ui-styling/canvas-fonts/BigShoulders-Bold.ttf create mode 100644 skills/website-creator/ui-styling/canvas-fonts/BigShoulders-OFL.txt create mode 100644 skills/website-creator/ui-styling/canvas-fonts/BigShoulders-Regular.ttf create mode 100644 skills/website-creator/ui-styling/canvas-fonts/Boldonse-OFL.txt create mode 100644 skills/website-creator/ui-styling/canvas-fonts/Boldonse-Regular.ttf create mode 100644 skills/website-creator/ui-styling/canvas-fonts/BricolageGrotesque-Bold.ttf create mode 100644 skills/website-creator/ui-styling/canvas-fonts/BricolageGrotesque-OFL.txt create mode 100644 skills/website-creator/ui-styling/canvas-fonts/BricolageGrotesque-Regular.ttf create mode 100644 skills/website-creator/ui-styling/canvas-fonts/CrimsonPro-Bold.ttf create mode 100644 skills/website-creator/ui-styling/canvas-fonts/CrimsonPro-Italic.ttf create mode 100644 skills/website-creator/ui-styling/canvas-fonts/CrimsonPro-OFL.txt create mode 100644 skills/website-creator/ui-styling/canvas-fonts/CrimsonPro-Regular.ttf create mode 100644 skills/website-creator/ui-styling/canvas-fonts/DMMono-OFL.txt create mode 100644 skills/website-creator/ui-styling/canvas-fonts/DMMono-Regular.ttf create mode 100644 skills/website-creator/ui-styling/canvas-fonts/EricaOne-OFL.txt create mode 100644 skills/website-creator/ui-styling/canvas-fonts/EricaOne-Regular.ttf create mode 100644 skills/website-creator/ui-styling/canvas-fonts/GeistMono-Bold.ttf create mode 100644 skills/website-creator/ui-styling/canvas-fonts/GeistMono-OFL.txt create mode 100644 skills/website-creator/ui-styling/canvas-fonts/GeistMono-Regular.ttf create mode 100644 skills/website-creator/ui-styling/canvas-fonts/Gloock-OFL.txt create mode 100644 skills/website-creator/ui-styling/canvas-fonts/Gloock-Regular.ttf create mode 100644 skills/website-creator/ui-styling/canvas-fonts/IBMPlexMono-Bold.ttf create mode 100644 skills/website-creator/ui-styling/canvas-fonts/IBMPlexMono-OFL.txt create mode 100644 skills/website-creator/ui-styling/canvas-fonts/IBMPlexMono-Regular.ttf create mode 100644 skills/website-creator/ui-styling/canvas-fonts/IBMPlexSerif-Bold.ttf create mode 100644 skills/website-creator/ui-styling/canvas-fonts/IBMPlexSerif-BoldItalic.ttf create mode 100644 skills/website-creator/ui-styling/canvas-fonts/IBMPlexSerif-Italic.ttf create mode 100644 skills/website-creator/ui-styling/canvas-fonts/IBMPlexSerif-Regular.ttf create mode 100644 skills/website-creator/ui-styling/canvas-fonts/InstrumentSans-Bold.ttf create mode 100644 skills/website-creator/ui-styling/canvas-fonts/InstrumentSans-BoldItalic.ttf create mode 100644 skills/website-creator/ui-styling/canvas-fonts/InstrumentSans-Italic.ttf create mode 100644 skills/website-creator/ui-styling/canvas-fonts/InstrumentSans-OFL.txt create mode 100644 skills/website-creator/ui-styling/canvas-fonts/InstrumentSans-Regular.ttf create mode 100644 skills/website-creator/ui-styling/canvas-fonts/InstrumentSerif-Italic.ttf create mode 100644 skills/website-creator/ui-styling/canvas-fonts/InstrumentSerif-Regular.ttf create mode 100644 skills/website-creator/ui-styling/canvas-fonts/Italiana-OFL.txt create mode 100644 skills/website-creator/ui-styling/canvas-fonts/Italiana-Regular.ttf create mode 100644 skills/website-creator/ui-styling/canvas-fonts/JetBrainsMono-Bold.ttf create mode 100644 skills/website-creator/ui-styling/canvas-fonts/JetBrainsMono-OFL.txt create mode 100644 skills/website-creator/ui-styling/canvas-fonts/JetBrainsMono-Regular.ttf create mode 100644 skills/website-creator/ui-styling/canvas-fonts/Jura-Light.ttf create mode 100644 skills/website-creator/ui-styling/canvas-fonts/Jura-Medium.ttf create mode 100644 skills/website-creator/ui-styling/canvas-fonts/Jura-OFL.txt create mode 100644 skills/website-creator/ui-styling/canvas-fonts/LibreBaskerville-OFL.txt create mode 100644 skills/website-creator/ui-styling/canvas-fonts/LibreBaskerville-Regular.ttf create mode 100644 skills/website-creator/ui-styling/canvas-fonts/Lora-Bold.ttf create mode 100644 skills/website-creator/ui-styling/canvas-fonts/Lora-BoldItalic.ttf create mode 100644 skills/website-creator/ui-styling/canvas-fonts/Lora-Italic.ttf create mode 100644 skills/website-creator/ui-styling/canvas-fonts/Lora-OFL.txt create mode 100644 skills/website-creator/ui-styling/canvas-fonts/Lora-Regular.ttf create mode 100644 skills/website-creator/ui-styling/canvas-fonts/NationalPark-Bold.ttf create mode 100644 skills/website-creator/ui-styling/canvas-fonts/NationalPark-OFL.txt create mode 100644 skills/website-creator/ui-styling/canvas-fonts/NationalPark-Regular.ttf create mode 100644 skills/website-creator/ui-styling/canvas-fonts/NothingYouCouldDo-OFL.txt create mode 100644 skills/website-creator/ui-styling/canvas-fonts/NothingYouCouldDo-Regular.ttf create mode 100644 skills/website-creator/ui-styling/canvas-fonts/Outfit-Bold.ttf create mode 100644 skills/website-creator/ui-styling/canvas-fonts/Outfit-OFL.txt create mode 100644 skills/website-creator/ui-styling/canvas-fonts/Outfit-Regular.ttf create mode 100644 skills/website-creator/ui-styling/canvas-fonts/PixelifySans-Medium.ttf create mode 100644 skills/website-creator/ui-styling/canvas-fonts/PixelifySans-OFL.txt create mode 100644 skills/website-creator/ui-styling/canvas-fonts/PoiretOne-OFL.txt create mode 100644 skills/website-creator/ui-styling/canvas-fonts/PoiretOne-Regular.ttf create mode 100644 skills/website-creator/ui-styling/canvas-fonts/RedHatMono-Bold.ttf create mode 100644 skills/website-creator/ui-styling/canvas-fonts/RedHatMono-OFL.txt create mode 100644 skills/website-creator/ui-styling/canvas-fonts/RedHatMono-Regular.ttf create mode 100644 skills/website-creator/ui-styling/canvas-fonts/Silkscreen-OFL.txt create mode 100644 skills/website-creator/ui-styling/canvas-fonts/Silkscreen-Regular.ttf create mode 100644 skills/website-creator/ui-styling/canvas-fonts/SmoochSans-Medium.ttf create mode 100644 skills/website-creator/ui-styling/canvas-fonts/SmoochSans-OFL.txt create mode 100644 skills/website-creator/ui-styling/canvas-fonts/Tektur-Medium.ttf create mode 100644 skills/website-creator/ui-styling/canvas-fonts/Tektur-OFL.txt create mode 100644 skills/website-creator/ui-styling/canvas-fonts/Tektur-Regular.ttf create mode 100644 skills/website-creator/ui-styling/canvas-fonts/WorkSans-Bold.ttf create mode 100644 skills/website-creator/ui-styling/canvas-fonts/WorkSans-BoldItalic.ttf create mode 100644 skills/website-creator/ui-styling/canvas-fonts/WorkSans-Italic.ttf create mode 100644 skills/website-creator/ui-styling/canvas-fonts/WorkSans-OFL.txt create mode 100644 skills/website-creator/ui-styling/canvas-fonts/WorkSans-Regular.ttf create mode 100644 skills/website-creator/ui-styling/canvas-fonts/YoungSerif-OFL.txt create mode 100644 skills/website-creator/ui-styling/canvas-fonts/YoungSerif-Regular.ttf create mode 100644 skills/website-creator/ui-styling/references/canvas-design-system.md create mode 100644 skills/website-creator/ui-styling/references/shadcn-accessibility.md create mode 100644 skills/website-creator/ui-styling/references/shadcn-components.md create mode 100644 skills/website-creator/ui-styling/references/shadcn-theming.md create mode 100644 skills/website-creator/ui-styling/references/tailwind-customization.md create mode 100644 skills/website-creator/ui-styling/references/tailwind-responsive.md create mode 100644 skills/website-creator/ui-styling/references/tailwind-utilities.md create mode 100644 skills/website-creator/ui-styling/scripts/.coverage create mode 100644 skills/website-creator/ui-styling/scripts/requirements.txt create mode 100644 skills/website-creator/ui-styling/scripts/shadcn_add.py create mode 100644 skills/website-creator/ui-styling/scripts/tailwind_config_gen.py create mode 100644 skills/website-creator/ui-styling/scripts/tests/coverage-ui.json create mode 100644 skills/website-creator/ui-styling/scripts/tests/requirements.txt create mode 100644 skills/website-creator/ui-styling/scripts/tests/test_shadcn_add.py create mode 100644 skills/website-creator/ui-styling/scripts/tests/test_tailwind_config_gen.py create mode 100644 skills/website-creator/ui-ux-pro-max/SKILL.md create mode 120000 skills/website-creator/ui-ux-pro-max/data create mode 120000 skills/website-creator/ui-ux-pro-max/scripts diff --git a/.DS_Store b/.DS_Store index 07895652877aaac9401c277c717325c21bb4b89c..c0395df9d0759f918e6f16b8e1250b9dac8d4591 100644 GIT binary patch delta 1003 zcmeH^%TE(g6vpo-kIv{d++tCLnaE2CrU4WQ)TpTtj8UXf+n_)oGt3o6+9`AzAc$#W zaN`3ZlV~FNTKS3#Mbx;^g~p&;aqGgE7~{g7i92sQb>rXQ<|f~F?#Vgl_ss-nf^(sg z0zwD~VT449txwpadNi&-9=qz9Pp@C{Ncj>9oLV;LX=>f+uV@K4uUOdG#=K6o_gjOH z(jL>`R?<9W@;#INQ5;rnk z23wZo)gtCC-dd@uy_rgTf{WLXq^w=HU8P~$oZ?K&eXMVwG(2Kj2D4?iwP_bsBT-G~ zQD(?)TWf%--QzrAn{kUB&Y63CR98<7#$z$Dvz;^dx2bA>W^SiuCqtv$5<9Krh93~M z9(!0z@X&#B4@8yfS1sR}2$v3=wd`Aoi(pX4`y zLdaN)b?~7KHSnVuEoemmZ3y8gj^Q}^Fbor?VPg_gID=`N#|2!%Wz6ClZr~Pf;|?C+ z37%pZ&+r;6c#jYGh%flM)aYA4N~K%=g8w&=h+xJirA!pevnqBiSE^`@B$%-RSJu)x zNzh`2uJkMQlE{ax$x6CIX_7?>EYFpGrA3w)T(O((kwqS?D&N&A4azbjS)sHm9g<+m zB&piv(i_=_gXj`>XReN*9|O>!!$1rcMlmJ`Oyew4NaGwX3IemZf_YrW0v2%-cX3Zp pc!)=W!E?O8OT58bymORIKjzcrOP`T3JdM zL1!5#=tR*)C%UM`if+27z`D(DyC{kxqKlyOMc?e$P5(ldzvunlkC)#w)-cv^I_S$L zgn$rANFPZOOSPi0hh& z(hy*Or{5NdKCwerH6y9-(zV2P#foZ%svAAwL0wJuwh!o*9*JooLy2pG7Zr9%Za|X# zVyCJkwN0k#Y^gk*5T3*t!7nEn(1LdVNt8!UNXBngITk;%xM~VNnVi; zh7&l6v$%*$ zxQr{fiMvQ)0{8F;lX!;bc!Ae=gSU8x&-jU7`0dH^Frpqya~&2~|$tm0Wm)RRV0s;L!NGc;owE9)4-1#!hn zfptXvnbTa-z?h^#lGZe{eK#9_chKeIB712~9L z9AZw6Gb^Vsj&nGV3(U+lT*nRE!foc|J|5s99^(m~+Q|Qy;tIZI+LpJ+UhnJq-)+4A E0|N#9+5i9m diff --git a/.env.example b/.env.example index dece3c2..a22e6c5 100644 --- a/.env.example +++ b/.env.example @@ -10,6 +10,11 @@ MINIMAX_API_KEY= MINIMAX_API_BASE=https://api.minimax.io/v1 +# =========================================== +# FAL AI (picture-it image generation) +# =========================================== +FAL_KEY= + # =========================================== # GITEA (Optional - Git sync) # =========================================== @@ -38,14 +43,6 @@ UMAMI_PASSWORD= ADMIN_PASSWORD= UMAMI_DOMAIN=analytics.example.com -# =========================================== -# SHODH MEMORY (Optional - Persistent context) -# =========================================== -SHODH_API_KEY= -SHODH_HOST=http://localhost -SHODH_PORT=3030 -SHODH_USER_ID=default - # =========================================== # GOOGLE ANALYTICS 4 (Optional) # =========================================== @@ -71,6 +68,11 @@ DATAFORSEO_BASE_URL=https://api.dataforseo.com # JINA API - Content extraction JINA_API_KEY= +# =========================================== +# DESIGN SKILLS (Logo, CIP, Icon generation) +# =========================================== +GEMINI_API_KEY= + # LLM Config (MiniMax default, OpenAI compatible) LLM_PROVIDER=minimax LLM_MODEL=MiniMax-Text-01 diff --git a/.opencode/package-lock.json b/.opencode/package-lock.json new file mode 100644 index 0000000..6aa38cb --- /dev/null +++ b/.opencode/package-lock.json @@ -0,0 +1,115 @@ +{ + "name": ".opencode", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "dependencies": { + "@opencode-ai/plugin": "1.3.15" + } + }, + "node_modules/@opencode-ai/plugin": { + "version": "1.3.15", + "resolved": "https://registry.npmjs.org/@opencode-ai/plugin/-/plugin-1.3.15.tgz", + "integrity": "sha512-jZJbuvUXc5Limz8pacQl+ffATjjKGlq+xaA4wTUeW+/spwOf7Yr5Ryyvan8eNlYM8wy6h5SLfznl1rlFpjYC8w==", + "license": "MIT", + "dependencies": { + "@opencode-ai/sdk": "1.3.15", + "zod": "4.1.8" + }, + "peerDependencies": { + "@opentui/core": ">=0.1.96", + "@opentui/solid": ">=0.1.96" + }, + "peerDependenciesMeta": { + "@opentui/core": { + "optional": true + }, + "@opentui/solid": { + "optional": true + } + } + }, + "node_modules/@opencode-ai/sdk": { + "version": "1.3.15", + "resolved": "https://registry.npmjs.org/@opencode-ai/sdk/-/sdk-1.3.15.tgz", + "integrity": "sha512-Uk59C7wsK20wpdr277yx7Xz7TqG5jGqlZUpSW3wDH/7a2K2iBg0lXc2wskHuCXLRXMhXpPZtb4a3SOpPENkkbg==", + "license": "MIT", + "dependencies": { + "cross-spawn": "7.0.6" + } + }, + "node_modules/cross-spawn": { + "version": "7.0.6", + "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz", + "integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==", + "license": "MIT", + "dependencies": { + "path-key": "^3.1.0", + "shebang-command": "^2.0.0", + "which": "^2.0.1" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/isexe": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", + "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", + "license": "ISC" + }, + "node_modules/path-key": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", + "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/shebang-command": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", + "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==", + "license": "MIT", + "dependencies": { + "shebang-regex": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/shebang-regex": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz", + "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/which": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", + "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", + "license": "ISC", + "dependencies": { + "isexe": "^2.0.0" + }, + "bin": { + "node-which": "bin/node-which" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/zod": { + "version": "4.1.8", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/colinhacks" + } + } + } +} diff --git a/output/test/results.json b/output/test/results.json deleted file mode 100644 index 954a584..0000000 --- a/output/test/results.json +++ /dev/null @@ -1,90 +0,0 @@ -{ - "topic": "test", - "generated_at": "2026-03-10T10:41:26.339482", - "channels": { - "facebook": { - "channel": "facebook", - "language": "th", - "variations": [ - { - "id": "facebook_var_1", - "created_at": "2026-03-10T10:41:26.339500", - "primary_text": "[Facebook Post 1] test...", - "headline": "[Headline] test", - "cta": "เรียนรู้เพิ่มเติม", - "hashtags": [ - "#test" - ], - "image": { - "path": "output/test/facebook/images/generated_20260310_104126.png" - } - }, - { - "id": "facebook_var_2", - "created_at": "2026-03-10T10:41:26.339584", - "primary_text": "[Facebook Post 2] test...", - "headline": "[Headline] test", - "cta": "เรียนรู้เพิ่มเติม", - "hashtags": [ - "#test" - ], - "image": { - "path": "output/test/facebook/images/generated_20260310_104126.png" - } - }, - { - "id": "facebook_var_3", - "created_at": "2026-03-10T10:41:26.339605", - "primary_text": "[Facebook Post 3] test...", - "headline": "[Headline] test", - "cta": "เรียนรู้เพิ่มเติม", - "hashtags": [ - "#test" - ], - "image": { - "path": "output/test/facebook/images/generated_20260310_104126.png" - } - }, - { - "id": "facebook_var_4", - "created_at": "2026-03-10T10:41:26.339620", - "primary_text": "[Facebook Post 4] test...", - "headline": "[Headline] test", - "cta": "เรียนรู้เพิ่มเติม", - "hashtags": [ - "#test" - ], - "image": { - "path": "output/test/facebook/images/generated_20260310_104126.png" - } - }, - { - "id": "facebook_var_5", - "created_at": "2026-03-10T10:41:26.339633", - "primary_text": "[Facebook Post 5] test...", - "headline": "[Headline] test", - "cta": "เรียนรู้เพิ่มเติม", - "hashtags": [ - "#test" - ], - "image": { - "path": "output/test/facebook/images/generated_20260310_104126.png" - } - } - ], - "api_ready": { - "platform": "meta", - "api_version": "v18.0", - "endpoint": "/act_{ad_account_id}/adcreatives", - "method": "POST", - "field_mapping": { - "primary_text": "body", - "headline": "title", - "cta": "call_to_action.type", - "image": "story_id or link_data.picture" - } - } - } - }, - "summary": {} -} \ No newline at end of file diff --git a/output/บรการ-podcast-hosting/results.json b/output/บรการ-podcast-hosting/results.json deleted file mode 100644 index 8991f09..0000000 --- a/output/บรการ-podcast-hosting/results.json +++ /dev/null @@ -1,437 +0,0 @@ -{ - "topic": "บริการ podcast hosting", - "generated_at": "2026-03-08T22:51:11.780847", - "channels": { - "facebook": { - "channel": "facebook", - "language": "th", - "variations": [ - { - "id": "facebook_var_1", - "created_at": "2026-03-08T22:51:11.780865", - "primary_text": "[Facebook Post 1] บริการ podcast hosting...", - "headline": "[Headline] บริการ podcast hosting", - "cta": "เรียนรู้เพิ่มเติม", - "hashtags": [ - "#บริการpodcasthosting" - ], - "image": { - "path": "output/บรการ-podcast-hosting/facebook/images/generated_20260308_225111.png" - } - }, - { - "id": "facebook_var_2", - "created_at": "2026-03-08T22:51:11.781143", - "primary_text": "[Facebook Post 2] บริการ podcast hosting...", - "headline": "[Headline] บริการ podcast hosting", - "cta": "เรียนรู้เพิ่มเติม", - "hashtags": [ - "#บริการpodcasthosting" - ], - "image": { - "path": "output/บรการ-podcast-hosting/facebook/images/generated_20260308_225111.png" - } - }, - { - "id": "facebook_var_3", - "created_at": "2026-03-08T22:51:11.781169", - "primary_text": "[Facebook Post 3] บริการ podcast hosting...", - "headline": "[Headline] บริการ podcast hosting", - "cta": "เรียนรู้เพิ่มเติม", - "hashtags": [ - "#บริการpodcasthosting" - ], - "image": { - "path": "output/บรการ-podcast-hosting/facebook/images/generated_20260308_225111.png" - } - }, - { - "id": "facebook_var_4", - "created_at": "2026-03-08T22:51:11.781186", - "primary_text": "[Facebook Post 4] บริการ podcast hosting...", - "headline": "[Headline] บริการ podcast hosting", - "cta": "เรียนรู้เพิ่มเติม", - "hashtags": [ - "#บริการpodcasthosting" - ], - "image": { - "path": "output/บรการ-podcast-hosting/facebook/images/generated_20260308_225111.png" - } - }, - { - "id": "facebook_var_5", - "created_at": "2026-03-08T22:51:11.781204", - "primary_text": "[Facebook Post 5] บริการ podcast hosting...", - "headline": "[Headline] บริการ podcast hosting", - "cta": "เรียนรู้เพิ่มเติม", - "hashtags": [ - "#บริการpodcasthosting" - ], - "image": { - "path": "output/บรการ-podcast-hosting/facebook/images/generated_20260308_225111.png" - } - } - ], - "api_ready": { - "platform": "meta", - "api_version": "v18.0", - "endpoint": "/act_{ad_account_id}/adcreatives", - "method": "POST", - "field_mapping": { - "primary_text": "body", - "headline": "title", - "cta": "call_to_action.type", - "image": "story_id or link_data.picture" - } - } - }, - "google_ads": { - "channel": "google_ads", - "language": "th", - "variations": [ - { - "id": "google_ads_var_1", - "created_at": "2026-03-08T22:51:11.781221", - "headlines": [ - { - "text": "[Headline 1] บริการ podcast hosting" - }, - { - "text": "[Headline 2] บริการ podcast hosting" - }, - { - "text": "[Headline 3] บริการ podcast hosting" - }, - { - "text": "[Headline 4] บริการ podcast hosting" - }, - { - "text": "[Headline 5] บริการ podcast hosting" - }, - { - "text": "[Headline 6] บริการ podcast hosting" - }, - { - "text": "[Headline 7] บริการ podcast hosting" - }, - { - "text": "[Headline 8] บริการ podcast hosting" - }, - { - "text": "[Headline 9] บริการ podcast hosting" - }, - { - "text": "[Headline 10] บริการ podcast hosting" - }, - { - "text": "[Headline 11] บริการ podcast hosting" - }, - { - "text": "[Headline 12] บริการ podcast hosting" - }, - { - "text": "[Headline 13] บริการ podcast hosting" - }, - { - "text": "[Headline 14] บริการ podcast hosting" - }, - { - "text": "[Headline 15] บริการ podcast hosting" - } - ], - "descriptions": [ - { - "text": "[Description 1] Learn more about บริการ podcast hosting" - }, - { - "text": "[Description 2] Learn more about บริการ podcast hosting" - }, - { - "text": "[Description 3] Learn more about บริการ podcast hosting" - }, - { - "text": "[Description 4] Learn more about บริการ podcast hosting" - } - ], - "keywords": [ - "บริการ podcast hosting", - "บริการ บริการ podcast hosting" - ], - "api_ready": { - "platform": "google", - "api_version": "v15.0", - "endpoint": "/google.ads.googleads.v15.services/GoogleAdsService:Mutate" - } - }, - { - "id": "google_ads_var_2", - "created_at": "2026-03-08T22:51:11.781228", - "headlines": [ - { - "text": "[Headline 1] บริการ podcast hosting" - }, - { - "text": "[Headline 2] บริการ podcast hosting" - }, - { - "text": "[Headline 3] บริการ podcast hosting" - }, - { - "text": "[Headline 4] บริการ podcast hosting" - }, - { - "text": "[Headline 5] บริการ podcast hosting" - }, - { - "text": "[Headline 6] บริการ podcast hosting" - }, - { - "text": "[Headline 7] บริการ podcast hosting" - }, - { - "text": "[Headline 8] บริการ podcast hosting" - }, - { - "text": "[Headline 9] บริการ podcast hosting" - }, - { - "text": "[Headline 10] บริการ podcast hosting" - }, - { - "text": "[Headline 11] บริการ podcast hosting" - }, - { - "text": "[Headline 12] บริการ podcast hosting" - }, - { - "text": "[Headline 13] บริการ podcast hosting" - }, - { - "text": "[Headline 14] บริการ podcast hosting" - }, - { - "text": "[Headline 15] บริการ podcast hosting" - } - ], - "descriptions": [ - { - "text": "[Description 1] Learn more about บริการ podcast hosting" - }, - { - "text": "[Description 2] Learn more about บริการ podcast hosting" - }, - { - "text": "[Description 3] Learn more about บริการ podcast hosting" - }, - { - "text": "[Description 4] Learn more about บริการ podcast hosting" - } - ], - "keywords": [ - "บริการ podcast hosting", - "บริการ บริการ podcast hosting" - ], - "api_ready": { - "platform": "google", - "api_version": "v15.0", - "endpoint": "/google.ads.googleads.v15.services/GoogleAdsService:Mutate" - } - }, - { - "id": "google_ads_var_3", - "created_at": "2026-03-08T22:51:11.781232", - "headlines": [ - { - "text": "[Headline 1] บริการ podcast hosting" - }, - { - "text": "[Headline 2] บริการ podcast hosting" - }, - { - "text": "[Headline 3] บริการ podcast hosting" - }, - { - "text": "[Headline 4] บริการ podcast hosting" - }, - { - "text": "[Headline 5] บริการ podcast hosting" - }, - { - "text": "[Headline 6] บริการ podcast hosting" - }, - { - "text": "[Headline 7] บริการ podcast hosting" - }, - { - "text": "[Headline 8] บริการ podcast hosting" - }, - { - "text": "[Headline 9] บริการ podcast hosting" - }, - { - "text": "[Headline 10] บริการ podcast hosting" - }, - { - "text": "[Headline 11] บริการ podcast hosting" - }, - { - "text": "[Headline 12] บริการ podcast hosting" - }, - { - "text": "[Headline 13] บริการ podcast hosting" - }, - { - "text": "[Headline 14] บริการ podcast hosting" - }, - { - "text": "[Headline 15] บริการ podcast hosting" - } - ], - "descriptions": [ - { - "text": "[Description 1] Learn more about บริการ podcast hosting" - }, - { - "text": "[Description 2] Learn more about บริการ podcast hosting" - }, - { - "text": "[Description 3] Learn more about บริการ podcast hosting" - }, - { - "text": "[Description 4] Learn more about บริการ podcast hosting" - } - ], - "keywords": [ - "บริการ podcast hosting", - "บริการ บริการ podcast hosting" - ], - "api_ready": { - "platform": "google", - "api_version": "v15.0", - "endpoint": "/google.ads.googleads.v15.services/GoogleAdsService:Mutate" - } - } - ], - "api_ready": { - "platform": "google", - "api_version": "v15.0", - "service": "GoogleAdsService", - "endpoint": "/google.ads.googleads.v15.services/GoogleAdsService:Mutate", - "resource_hierarchy": [ - "customer", - "campaign", - "ad_group", - "ad_group_ad", - "ad (RESPONSIVE_SEARCH_AD)" - ], - "field_mapping": { - "headlines": "responsive_search_ad.headlines", - "descriptions": "responsive_search_ad.descriptions", - "final_url": "responsive_search_ad.final_urls", - "display_path": "responsive_search_ad.path1, path2", - "keywords": "ad_group_criterion", - "bid_modifier": "ad_group_criterion.cpc_bid_modifier" - }, - "future_integration_notes": [ - "Add conversion_tracking_setup", - "Add value_track_parameters", - "Add ad_schedule_bid_modifiers", - "Add device_bid_modifiers", - "Add location_bid_modifiers", - "Setup enhanced conversions" - ] - } - }, - "blog": { - "channel": "blog", - "language": "th", - "variations": [ - { - "id": "blog_var_1", - "created_at": "2026-03-08T22:51:11.781238", - "markdown": "---\ntitle: \"บริการ podcast hosting - Complete Guide\"\ndescription: \"Learn everything about บริการ podcast hosting in this comprehensive guide\"\nkeywords: [\"บริการ podcast hosting\", \"บริการ บริการ podcast hosting\", \"guide\"]\nslug: บรการ-podcast-hosting\nlang: th\ncategory: guides\ntags: [\"บริการ podcast hosting\", \"guide\"]\ncreated: 2026-03-08\n---\n\n# บริการ podcast hosting: Complete Guide\n\n## Introduction\n\n[Opening hook about บริการ podcast hosting...]\n\n## What is บริการ podcast hosting?\n\n[Definition and explanation...]\n\n## Why บริการ podcast hosting Matters\n\n[Importance and benefits...]\n\n## How to Get Started with บริการ podcast hosting\n\n[Step-by-step guide...]\n\n## Best Practices for บริการ podcast hosting\n\n[Tips and recommendations...]\n\n## Conclusion\n\n[Summary and call-to-action...]\n", - "frontmatter": { - "title": "บริการ podcast hosting - Complete Guide", - "description": "Learn about บริการ podcast hosting", - "slug": "บรการ-podcast-hosting", - "lang": "th" - }, - "word_count": 1500, - "publish_status": "draft" - }, - { - "id": "blog_var_2", - "created_at": "2026-03-08T22:51:11.781250", - "markdown": "---\ntitle: \"บริการ podcast hosting - Complete Guide\"\ndescription: \"Learn everything about บริการ podcast hosting in this comprehensive guide\"\nkeywords: [\"บริการ podcast hosting\", \"บริการ บริการ podcast hosting\", \"guide\"]\nslug: บรการ-podcast-hosting\nlang: th\ncategory: guides\ntags: [\"บริการ podcast hosting\", \"guide\"]\ncreated: 2026-03-08\n---\n\n# บริการ podcast hosting: Complete Guide\n\n## Introduction\n\n[Opening hook about บริการ podcast hosting...]\n\n## What is บริการ podcast hosting?\n\n[Definition and explanation...]\n\n## Why บริการ podcast hosting Matters\n\n[Importance and benefits...]\n\n## How to Get Started with บริการ podcast hosting\n\n[Step-by-step guide...]\n\n## Best Practices for บริการ podcast hosting\n\n[Tips and recommendations...]\n\n## Conclusion\n\n[Summary and call-to-action...]\n", - "frontmatter": { - "title": "บริการ podcast hosting - Complete Guide", - "description": "Learn about บริการ podcast hosting", - "slug": "บรการ-podcast-hosting", - "lang": "th" - }, - "word_count": 1500, - "publish_status": "draft" - }, - { - "id": "blog_var_3", - "created_at": "2026-03-08T22:51:11.781259", - "markdown": "---\ntitle: \"บริการ podcast hosting - Complete Guide\"\ndescription: \"Learn everything about บริการ podcast hosting in this comprehensive guide\"\nkeywords: [\"บริการ podcast hosting\", \"บริการ บริการ podcast hosting\", \"guide\"]\nslug: บรการ-podcast-hosting\nlang: th\ncategory: guides\ntags: [\"บริการ podcast hosting\", \"guide\"]\ncreated: 2026-03-08\n---\n\n# บริการ podcast hosting: Complete Guide\n\n## Introduction\n\n[Opening hook about บริการ podcast hosting...]\n\n## What is บริการ podcast hosting?\n\n[Definition and explanation...]\n\n## Why บริการ podcast hosting Matters\n\n[Importance and benefits...]\n\n## How to Get Started with บริการ podcast hosting\n\n[Step-by-step guide...]\n\n## Best Practices for บริการ podcast hosting\n\n[Tips and recommendations...]\n\n## Conclusion\n\n[Summary and call-to-action...]\n", - "frontmatter": { - "title": "บริการ podcast hosting - Complete Guide", - "description": "Learn about บริการ podcast hosting", - "slug": "บรการ-podcast-hosting", - "lang": "th" - }, - "word_count": 1500, - "publish_status": "draft" - }, - { - "id": "blog_var_4", - "created_at": "2026-03-08T22:51:11.781272", - "markdown": "---\ntitle: \"บริการ podcast hosting - Complete Guide\"\ndescription: \"Learn everything about บริการ podcast hosting in this comprehensive guide\"\nkeywords: [\"บริการ podcast hosting\", \"บริการ บริการ podcast hosting\", \"guide\"]\nslug: บรการ-podcast-hosting\nlang: th\ncategory: guides\ntags: [\"บริการ podcast hosting\", \"guide\"]\ncreated: 2026-03-08\n---\n\n# บริการ podcast hosting: Complete Guide\n\n## Introduction\n\n[Opening hook about บริการ podcast hosting...]\n\n## What is บริการ podcast hosting?\n\n[Definition and explanation...]\n\n## Why บริการ podcast hosting Matters\n\n[Importance and benefits...]\n\n## How to Get Started with บริการ podcast hosting\n\n[Step-by-step guide...]\n\n## Best Practices for บริการ podcast hosting\n\n[Tips and recommendations...]\n\n## Conclusion\n\n[Summary and call-to-action...]\n", - "frontmatter": { - "title": "บริการ podcast hosting - Complete Guide", - "description": "Learn about บริการ podcast hosting", - "slug": "บรการ-podcast-hosting", - "lang": "th" - }, - "word_count": 1500, - "publish_status": "draft" - }, - { - "id": "blog_var_5", - "created_at": "2026-03-08T22:51:11.781279", - "markdown": "---\ntitle: \"บริการ podcast hosting - Complete Guide\"\ndescription: \"Learn everything about บริการ podcast hosting in this comprehensive guide\"\nkeywords: [\"บริการ podcast hosting\", \"บริการ บริการ podcast hosting\", \"guide\"]\nslug: บรการ-podcast-hosting\nlang: th\ncategory: guides\ntags: [\"บริการ podcast hosting\", \"guide\"]\ncreated: 2026-03-08\n---\n\n# บริการ podcast hosting: Complete Guide\n\n## Introduction\n\n[Opening hook about บริการ podcast hosting...]\n\n## What is บริการ podcast hosting?\n\n[Definition and explanation...]\n\n## Why บริการ podcast hosting Matters\n\n[Importance and benefits...]\n\n## How to Get Started with บริการ podcast hosting\n\n[Step-by-step guide...]\n\n## Best Practices for บริการ podcast hosting\n\n[Tips and recommendations...]\n\n## Conclusion\n\n[Summary and call-to-action...]\n", - "frontmatter": { - "title": "บริการ podcast hosting - Complete Guide", - "description": "Learn about บริการ podcast hosting", - "slug": "บรการ-podcast-hosting", - "lang": "th" - }, - "word_count": 1500, - "publish_status": "draft" - } - ], - "api_ready": { - "cms_compatible": [ - "WordPress", - "Contentful", - "Sanity", - "Strapi" - ], - "schema_org": { - "type": "BlogPosting", - "required_fields": [ - "headline", - "description", - "image", - "datePublished", - "author", - "publisher" - ] - } - } - } - }, - "summary": {} -} \ No newline at end of file diff --git a/scripts/install-skills.sh b/scripts/install-skills.sh index bb87747..81e7778 100755 --- a/scripts/install-skills.sh +++ b/scripts/install-skills.sh @@ -51,6 +51,15 @@ setup_unified_env() { [ -f "$env_example" ] || return + # Check if .env already exists in repo - skip interactive setup if it does + if [ -f "$env_file" ]; then + line + print_success "Using existing .env file in project" + line + echo "" + return + fi + line print_info "Unified Configuration Setup" line diff --git a/skills/.DS_Store b/skills/.DS_Store index 172f1a413b67a0d0bf69a09e0ff778062381b9f5..9ffc065e28c2e260ad9af6180ab3a42c5a057d3a 100644 GIT binary patch literal 10244 zcmeHMU2GIp6uxI#V1^F#7K$yn6Bi2MCtD~KZOf0@{wW|(U|YH^g=KbUqyw`v%g*dB zr8G7sK8S+xN#nmy8i_m@5aWv`Dx!~yCKzK3QRB}8KKP>Y;JNqCHk5*d#27I%$-U>E zd(XK)-<)sSbC(bT9R*`GAyGnzjD<<6Vs(Rujk7Z4k%AsJ2UL?A>UL?A@qzkmS#&1R2WC1hBK2!sfP2uvWrz7J6>OvVE_E+qeSV8v4al$F@c zA8cbDzL4?9E{&?~)P2%h+kxwV8Vj8k*SINV24W!#=UF zogu?K_2r;5xH`b2#kTempmZQx;h#ZG}oMWT$yS7khu znN{j6tv@+1s1Bv*oFX2)C+AzPN{i=P$TBqG>lXO%~GD;ezFl&<0uNYeW}n>X3n( z+vXZ<6@#9YaUNWK$wE!r!$vyuCe}vauzZE4_Ih{*st1NESE*|6Aq*kvu`Qr$^IBEw zv-PxTQ$rE!w6>{Qe^KC19|}4*)~Z@E<7TsXL@(%T@6fb;Y}u{4=lA4H2e)(~So;AS z8}x?tyxCL25Dt2`U@w2A*w3804a;}=AfmAyPcguzYc~vjh6VNWtF?sIC)3Yld_|U1 zw2(IVS_OOLhVrXvh_4XMhW8JD%Uc|1r6JxFn8-vUYe*N_O7@dH86_vkv*Z*xLp~zs z$rt1s@)Nm2eg}Z*pukM1f?8Mriy;R0LJPD)8+1Sq?1WvAfFT$L3l4$@N8l)o!ZCOZ zj>D610#3p+@FKhnufVJD2D}Al;4Hii@4*H51U`k&;WB&&-@^}Z1^$3P;i@!Es*~!a z`O+dOCN)Ycr534GS|_cSwo5%yQqrY^k}KuJ{-rWk_Xg()gfxPUTW}Q#7<`bV(>Eqy z=oD7dNK zQ|9gUB<({AnMY*CWI97wFH#rFNFh2+s7ux5GLnZ*7iuiBQbxK`iL2`BNV9^Zq!mJq zMOqY@f>73}8x*7`trV)J#ubGUu0%Q`-7->)%KudCUm};ukK|W!6=Q!k%!PVb3QZXC z_roUGjxo<7z7O_63Uo+=0aC7UVxY2HFzBv za0=eU_&*2lz`O7PdAp*CB091A*x;oJ1X8+=|yY?uS z{aDz;?8b%UE?DvFc$EA)9u;=4xC{%kE#DEac0A}C7ZQ8$`PY982=o8Hy?_hz|Nnpf F{}(7R;r0Lk literal 6148 zcmeHKQA)!=5S^_>Qv5Lf`6KMV2o^68OFTg@VAE7vNV~zR(7&9(i+BLNf!FfQ&QMyS zQ2h{<8JKyS*_mwiE$n27$PFIKK2b_U36!zZgZV&sp0y<%_3Q&(d`C?qTF`_lI%`Fn z<2N$EXP4r{Mzo|AZJu8VZ;g`-v?`b5stBRt4II%uO%XoB5#P`qrWf!`U`u#7>G6x< z)op(`4)a>*IeFb}k$K4Tq6!6~lb6%Oi>K$e%Uv_eZ)Voh%`ZKd4>oHI7z4(@-(vtZ zn` and end with ``. -2. Do not use compressed XML. Use plain XML. -3. Use standard shapes: `rounded=1;whiteSpace=wrap;html=1;` for boxes. -4. **Auto-layout Strategy**: - - Identify "layers" or "stages" in the logic. - - Assign X coordinates based on layers (e.g., 0, 200, 400). - - Assign Y coordinates to distribute nodes vertically (e.g., 0, 100, 200). - - Ensure nodes do not overlap. -5. **Edges**: Connect nodes logically using ``. - -### Template: - - - - - - - - - - - - - - - - -``` - -**Task Input:** -```markdown -Please generate a Draw.io XML diagram for the following logic flow: - -**Title**: {title} - -**Nodes and Logic**: -{nodes_json} - -Ensure the layout flows logically from Left to Right (or Top to Bottom for hierarchies). -Use different colors for 'Positive' (Green/fillColor=#d5e8d4), 'Negative' (Red/fillColor=#f8cecc), and 'Neutral' (Grey/fillColor=#f5f5f5) impacts. -``` diff --git a/skills/alphaear-logic-visualizer/scripts/__init__.py b/skills/alphaear-logic-visualizer/scripts/__init__.py deleted file mode 100644 index e69de29..0000000 diff --git a/skills/alphaear-logic-visualizer/scripts/visualizer.py b/skills/alphaear-logic-visualizer/scripts/visualizer.py deleted file mode 100644 index 85a38cd..0000000 --- a/skills/alphaear-logic-visualizer/scripts/visualizer.py +++ /dev/null @@ -1,472 +0,0 @@ -import os -from typing import Dict, List, Any, Optional -import pandas as pd -from loguru import logger -from pyecharts.charts import Kline, Line, Bar, Grid, Radar, Graph -from pyecharts import options as opts -from pyecharts.globals import ThemeType -from datetime import datetime, timedelta - -class VisualizerTools: - """可视化工具库 - 使用 Pyecharts 生成 HTML 图表""" - - @staticmethod - def generate_stock_chart( - df: pd.DataFrame, - ticker: str, - title: str = None, - prediction: Optional[List[float]] = None, - forecast: Optional[Any] = None, # ForecastResult instance - ground_truth: Optional[pd.DataFrame] = None # For training visualization - ) -> Grid: - """ - 生成股票 K 线图 + 成交量 + 预测趋势 (支持多状态 K 线) - """ - if df.empty: - return None - - # 数据预处理 - df = df.sort_values('date') - dates = [str(d)[:10] for d in df['date'].tolist()] - k_data = df[['open', 'close', 'low', 'high']].values.tolist() - volumes = df['volume'].tolist() - - if not title: - title = f"{ticker} 股价走势与预测" - - legend_items = ["日K"] - - # 1. 处理传统的简单预测线 (Line) - pred_line = None - if prediction and not forecast: - try: - last_date_str = dates[-1] - last_date = datetime.strptime(last_date_str, "%Y-%m-%d") - - pred_dates = [] - for i in range(1, len(prediction) + 1): - pred_dates.append((last_date + timedelta(days=i)).strftime("%Y-%m-%d")) - - ext_dates = dates + pred_dates - last_close = df.iloc[-1]['close'] - pred_values = [None] * (len(df) - 1) + [float(last_close)] + prediction - - pred_line = ( - Line() - .add_xaxis(ext_dates) - .add_yaxis( - "AI预测趋势", - pred_values, - is_connect_nones=True, - is_symbol_show=True, - linestyle_opts=opts.LineStyleOpts(width=2, type_="dashed", color="#FF8C00"), - label_opts=opts.LabelOpts(is_show=False) - ) - ) - dates = ext_dates - legend_items.append("AI预测趋势") - except Exception as e: - logger.error(f"Failed to process simple prediction: {e}") - - # 2. 处理复杂的 Kronos 预测 (Kline) - base_kline = None - adj_kline = None - - if forecast: - try: - # 获取预测数据点 - base_points = forecast.base_forecast # List[KLinePoint] - adj_points = forecast.adjusted_forecast # List[KLinePoint] - - # 提取日期 - pred_dates = [str(p.date)[:10] for p in (adj_points or base_points)] - - # 检查日期是否已经包含在主 dates 中,如果没有则扩展 - if pred_dates and pred_dates[0] not in dates: - dates = dates + pred_dates - - # 构建 Baseline 预测 K 线数据 - if base_points: - # 前面填充 None - base_k_data = [[None]*4] * len(df) + [[p.open, p.close, p.low, p.high] for p in base_points] - base_kline = ( - Kline() - .add_xaxis(dates) - .add_yaxis( - "模型原始预测", - base_k_data, - itemstyle_opts=opts.ItemStyleOpts( - color="transparent", - color0="transparent", - border_color="#FF8C00", # 橙色 - border_color0="#FF8C00", - opacity=0.6, - border_type="dashed" - ), - ) - ) - legend_items.append("模型原始预测") - - # 构建 Adjusted 调优 K 线数据 - if adj_points: - adj_k_data = [[None]*4] * len(df) + [[p.open, p.close, p.low, p.high] for p in adj_points] - adj_kline = ( - Kline() - .add_xaxis(dates) - .add_yaxis( - "LLM调优预测", - adj_k_data, - itemstyle_opts=opts.ItemStyleOpts( - color="#9333ea", # 紫色 - color0="#9333ea", - border_color="#9333ea", - border_color0="#9333ea", - opacity=0.8 - ), - ) - ) - legend_items.append("LLM调优预测") - - except Exception as e: - logger.error(f"Failed to process complex forecast: {e}") - - # 2.5 处理 Ground Truth (用于训练评估可视化) - gt_line = None - if ground_truth is not None and not ground_truth.empty: - try: - gt_dates = [str(d)[:10] for d in ground_truth['date'].tolist()] - # 确保日期包含在 dates 中 - for d in gt_dates: - if d not in dates: - dates.append(d) - dates = sorted(list(set(dates))) # Re-sort to maintain order - - gt_values = [None] * len(dates) - for _, row in ground_truth.iterrows(): - d_str = str(row['date'])[:10] - if d_str in dates: - idx = dates.index(d_str) - gt_values[idx] = float(row['close']) - - gt_line = ( - Line() - .add_xaxis(dates) - .add_yaxis( - "真实走势 (GT)", - gt_values, - is_connect_nones=True, - linestyle_opts=opts.LineStyleOpts(width=3, color="#2ecc71"), # 绿色粗线 - label_opts=opts.LabelOpts(is_show=False) - ) - ) - legend_items.append("真实走势 (GT)") - except Exception as e: - logger.error(f"Failed to process ground truth: {e}") - - # 3. 主 K 线图 - # 为了展示预测,也需要对主 K 线数据进行填充 - main_k_data = k_data + [[None]*4] * (len(dates) - len(df)) - - kline = ( - Kline() - .add_xaxis(dates) - .add_yaxis( - "日K", - main_k_data, - itemstyle_opts=opts.ItemStyleOpts( - color="#ef4444", # 跌 - color0="#22c55e", # 涨 - border_color="#ef4444", - border_color0="#22c55e", - ), - ) - .set_global_opts( - title_opts=opts.TitleOpts(title=title, pos_left="center"), - xaxis_opts=opts.AxisOpts(is_scale=True), - yaxis_opts=opts.AxisOpts( - is_scale=True, - splitarea_opts=opts.SplitAreaOpts( - is_show=True, areastyle_opts=opts.AreaStyleOpts(opacity=1) - ), - ), - legend_opts=opts.LegendOpts(is_show=True, pos_top="5%"), - datazoom_opts=[opts.DataZoomOpts(type_="inside", range_start=50)], - tooltip_opts=opts.TooltipOpts(trigger="axis", axis_pointer_type="cross"), - ) - ) - - # Overlap all series - if pred_line: kline.overlap(pred_line) - if base_kline: kline.overlap(base_kline) - if adj_kline: kline.overlap(adj_kline) - if gt_line: kline.overlap(gt_line) - - # 4. 成交量柱状图 - # 同理扩展成交量数据 - ext_volumes = volumes + [0] * (len(dates) - len(df)) - - bar = ( - Bar() - .add_xaxis(dates) - .add_yaxis( - "成交量", - ext_volumes, - xaxis_index=1, - yaxis_index=1, - label_opts=opts.LabelOpts(is_show=False), - itemstyle_opts=opts.ItemStyleOpts(color="#7fbe9e"), - ) - .set_global_opts( - xaxis_opts=opts.AxisOpts( - type_="category", - grid_index=1, - axislabel_opts=opts.LabelOpts(is_show=False), - ), - legend_opts=opts.LegendOpts(is_show=False), - ) - ) - - # 5. 组合 Grid - grid_chart = Grid(init_opts=opts.InitOpts(width="100%", height="450px", theme=ThemeType.LIGHT)) - grid_chart.add( - kline, - grid_opts=opts.GridOpts(pos_left="10%", pos_right="8%", height="50%"), - ) - grid_chart.add( - bar, - grid_opts=opts.GridOpts( - pos_left="10%", pos_right="8%", pos_top="65%", height="20%" - ), - ) - - return grid_chart - - @staticmethod - def generate_loss_chart(losses: List[float], title: str = "训练损失收敛曲线") -> Line: - """生成 Loss 下降曲线图""" - line = ( - Line(init_opts=opts.InitOpts(width="100%", height="400px", theme=ThemeType.LIGHT)) - .add_xaxis(list(range(1, len(losses) + 1))) - .add_yaxis( - "Training Loss", - losses, - is_smooth=True, - linestyle_opts=opts.LineStyleOpts(width=2, color="#3b82f6"), - label_opts=opts.LabelOpts(is_show=False), - markpoint_opts=opts.MarkPointOpts(data=[opts.MarkPointItem(type_="min", name="最小值")]) - ) - .set_global_opts( - title_opts=opts.TitleOpts(title=title, pos_left="center"), - xaxis_opts=opts.AxisOpts(name="Epoch", is_scale=True), - yaxis_opts=opts.AxisOpts(name="Loss", is_scale=True), - tooltip_opts=opts.TooltipOpts(trigger="axis"), - ) - ) - return line - - @staticmethod - def generate_sentiment_trend_chart(sentiment_history: List[Dict[str, Any]]) -> Line: - """ - 生成舆情情绪趋势图 - :param sentiment_history: [{"date": "2024-01-01", "score": 0.8}, ...] - """ - dates = [item['date'] for item in sentiment_history] - scores = [item['score'] for item in sentiment_history] - - line = ( - Line(init_opts=opts.InitOpts(width="100%", height="300px", theme=ThemeType.LIGHT)) - .add_xaxis(dates) - .add_yaxis( - "情绪指数", - scores, - is_smooth=True, - markline_opts=opts.MarkLineOpts(data=[opts.MarkLineItem(y=0, name="中性线")]), - itemstyle_opts=opts.ItemStyleOpts(color="#5470c6"), - areastyle_opts=opts.AreaStyleOpts(opacity=0.3, color="#5470c6") - ) - .set_global_opts( - title_opts=opts.TitleOpts(title="舆情情绪趋势", pos_left="center"), - legend_opts=opts.LegendOpts(pos_top="8%"), - yaxis_opts=opts.AxisOpts(min_=-1, max_=1, name="Sentiment"), - tooltip_opts=opts.TooltipOpts(trigger="axis"), - ) - ) - return line - - @staticmethod - def generate_isq_radar_chart(sentiment: float, confidence: float, intensity: int, - expectation_gap: float = 0.5, timeliness: float = 0.8, - title: str = "信号质量 ISQ 评估") -> Radar: - """生成信号质量雷达图""" - # 标准化数据 (0-100) - # sentiment 强度: 绝对值越大强度越高 - sent_val = min(100, abs(sentiment) * 100) - # confidence: 0 to 1 -> 0 to 100 - conf_val = confidence * 100 - # intensity: 1 to 5 -> 20 to 100 - int_val = intensity * 20 - # gap & time: 0 to 1 -> 0 to 100 - gap_val = expectation_gap * 100 - time_val = timeliness * 100 - - schema = [ - opts.RadarIndicatorItem(name="情绪强度", max_=100), - opts.RadarIndicatorItem(name="确定性", max_=100), - opts.RadarIndicatorItem(name="影响力", max_=100), - opts.RadarIndicatorItem(name="预期差", max_=100), - opts.RadarIndicatorItem(name="时效性", max_=100), - ] - - radar = ( - Radar(init_opts=opts.InitOpts(width="100%", height="400px", theme=ThemeType.LIGHT)) - .add_schema(schema=schema) - .add( - "信号特征", - [[sent_val, conf_val, int_val, gap_val, time_val]], - color="#f97316", - areastyle_opts=opts.AreaStyleOpts(opacity=0.3, color="#fb923c"), - ) - .set_global_opts( - title_opts=opts.TitleOpts(title=title, pos_left="center"), - legend_opts=opts.LegendOpts(is_show=False), - ) - ) - return radar - - @staticmethod - def generate_transmission_graph(nodes_data: List[Dict[str, str]], title: str = "投资逻辑传导链条") -> Graph: - """生成逻辑传导拓扑图 (支持分支结构)""" - nodes = [] - links = [] - - # Helper for text wrapping - def wrap_text(text, width=6): - return '\n'.join([text[i:i+width] for i in range(0, len(text), width)]) - - # Map original names to wrapped names to handle links - name_map = {} - - for i, item in enumerate(nodes_data): - # 节点样式 - color = "#ef4444" if "利空" in item.get("impact_type", "") else "#22c55e" - if "中性" in item.get("impact_type", ""): color = "#6b7280" - - original_name = item.get("node_name", f"节点{i}") - wrapped_name = wrap_text(original_name) - name_map[original_name] = wrapped_name - name_map[str(item.get("id", ""))] = wrapped_name # Map ID if present - - nodes.append({ - "name": wrapped_name, - "symbolSize": 60 if i == 0 else 50, - "value": item.get("logic", ""), - "itemStyle": {"color": color}, - # Improve label readability - "label": {"show": True, "formatter": "{b}"} - }) - - # Logic for Links - source_key = item.get("source") or item.get("parent") or item.get("parent_id") - if source_key: - # Branching logic: Link from specified source - # Source needs to be resolved to its (wrapped) name - target_source_name = name_map.get(source_key) - if not target_source_name and source_key in name_map.values(): - target_source_name = source_key # It was already a mapped name? - - # If we found the source in our map (meaning it appeared before this node) - if target_source_name: - links.append({"source": target_source_name, "target": wrapped_name}) - elif i > 0: - # Fallback: Linear chain - links.append({"source": nodes[i-1]["name"], "target": wrapped_name}) - - graph = ( - Graph(init_opts=opts.InitOpts(width="100%", height="400px", theme=ThemeType.LIGHT)) - .add( - "", - nodes, - links, - repulsion=5000, - layout="force", - is_roam=True, - is_draggable=True, - symbol="circle", - edge_symbol=['circle', 'arrow'], # Add arrows - edge_symbol_size=[4, 10], - linestyle_opts=opts.LineStyleOpts(width=2, curve=0.2, opacity=0.9), - label_opts=opts.LabelOpts(is_show=True, position="inside", color="white", font_size=10), - edge_label=opts.LabelOpts(is_show=False), - ) - .set_global_opts( - title_opts=opts.TitleOpts(title=title, pos_left="center"), - tooltip_opts=opts.TooltipOpts(formatter="{b}: {c}") - ) - ) - return graph - - @staticmethod - def render_drawio_to_html(xml_content: str, filename: str, title: str = "Logic Diagram") -> str: - """ - 将 Draw.io XML 渲染为包含 Viewer 的 HTML 文件 - """ - import json - - # 构造配置字典 - config = { - "highlight": "#0000ff", - "nav": True, - "resize": True, - "toolbar": "zoom", - "xml": xml_content - } - - # 1. 转为 JSON 字符串 (自动处理内部的引号转义、换行符转义等) - json_str = json.dumps(config) - - # 2. 转为 HTML 属性安全的字符串 (主要是转义单引号,因为我们在 HTML 中用单引号包裹) - import html - safe_json_str = html.escape(json_str, quote=True) - - html_template = f""" - - - - - {title} - - - -

{title}

-
- - - - """ - - try: - os.makedirs(os.path.dirname(filename), exist_ok=True) - # Use 'w' mode with utf-8 encoding - with open(filename, 'w', encoding='utf-8') as f: - f.write(html_template) - logger.info(f"✅ Draw.io chart rendered to {filename}") - return filename - except Exception as e: - logger.error(f"Failed to render drawio chart: {e}") - return "" - - @staticmethod - def render_chart_to_file(chart: Any, filename: str) -> str: - """渲染并保存 HTML""" - try: - # 确保目录存在 - os.makedirs(os.path.dirname(filename), exist_ok=True) - chart.render(filename) - logger.info(f"✅ Chart rendered to {filename}") - return filename - except Exception as e: - logger.error(f"Failed to render chart: {e}") - return "" diff --git a/skills/alphaear-logic-visualizer/scripts/visualizer_prompt.py b/skills/alphaear-logic-visualizer/scripts/visualizer_prompt.py deleted file mode 100644 index f0b2933..0000000 --- a/skills/alphaear-logic-visualizer/scripts/visualizer_prompt.py +++ /dev/null @@ -1,47 +0,0 @@ -def get_drawio_system_prompt(): - return """You are an expert at creating Draw.io (MxGraph) diagrams in XML format. -Your task is to generate a valid MXGraphModel XML based on the user's description. - -### Rules: -1. Output ONLY the XML code. Start with and end with . -2. Do not use compressed XML. Use plain XML. -3. Use standard shapes: 'rounded=1;whiteSpace=wrap;html=1;' for boxes. -4. Auto-layout Strategy: - - Identify "layers" or "stages" in the logic. - - Assign X coordinates based on layers (e.g., 0, 200, 400). - - Assign Y coordinates to distribute nodes vertically (e.g., 0, 100, 200). - - Ensure nodes do not overlap. -5. Edges: Connect nodes logically using . - -### Template: - - - - - - - - - - - - - - - - -""" - -def get_drawio_task(nodes_data: list, title: str) -> str: - import json - nodes_json = json.dumps(nodes_data, ensure_ascii=False, indent=2) - return f"""Please generate a Draw.io XML diagram for the following logic flow: - -**Title**: {title} - -**Nodes and Logic**: -{nodes_json} - -Ensure the layout flows logically from Left to Right (or Top to Bottom for hierarchies). -Use different colors for 'Positive' (Greenish), 'Negative' (Reddish), and 'Neutral' (Grey/Blue) impacts if described. -""" diff --git a/skills/alphaear-logic-visualizer/tests/test_visualizer.py b/skills/alphaear-logic-visualizer/tests/test_visualizer.py deleted file mode 100644 index 9b9731d..0000000 --- a/skills/alphaear-logic-visualizer/tests/test_visualizer.py +++ /dev/null @@ -1,21 +0,0 @@ -import sys -import os -import unittest - -# Add skill root to path -sys.path.append(os.path.abspath(os.path.join(os.path.dirname(__file__), '..'))) - -try: - from scripts.visualizer import VisualizerTools -except ImportError as e: - print(f"Import Error: {e}") - sys.exit(1) - -class TestLogicViz(unittest.TestCase): - def test_init(self): - print("Testing VisualizerTools Iteration...") - viz = VisualizerTools() - self.assertIsNotNone(viz) - -if __name__ == '__main__': - unittest.main() diff --git a/skills/alphaear-news/SKILL.md b/skills/alphaear-news/SKILL.md deleted file mode 100644 index e4130b4..0000000 --- a/skills/alphaear-news/SKILL.md +++ /dev/null @@ -1,33 +0,0 @@ ---- -name: alphaear-news -description: Fetch hot finance news, unified trends, and prediction financial market data. Use when the user needs real-time financial news, trend reports from multiple finance sources (Weibo, Zhihu, WallstreetCN, etc.), or Polymarket finance market prediction data. ---- - -# AlphaEar News Skill - -## Overview - -Fetch real-time hot news, generate unified trend reports, and retrieve Polymarket prediction data. - -## Capabilities - -### 1. Fetch Hot News & Trends - -Use `scripts/news_tools.py` via `NewsNowTools`. - -- **Fetch News**: `fetch_hot_news(source_id, count)` - - See [sources.md](references/sources.md) for valid `source_id`s (e.g., `cls`, `weibo`). -- **Unified Report**: `get_unified_trends(sources)` - - Aggregates top news from multiple sources. - -### 2. Fetch Prediction Markets - -Use `scripts/news_tools.py` via `PolymarketTools`. - -- **Market Summary**: `get_market_summary(limit)` - - Returns a formatted report of active prediction markets. - -## Dependencies - -- `requests`, `loguru` -- `scripts/database_manager.py` (Local DB) diff --git a/skills/alphaear-news/references/sources.md b/skills/alphaear-news/references/sources.md deleted file mode 100644 index d2c2677..0000000 --- a/skills/alphaear-news/references/sources.md +++ /dev/null @@ -1,26 +0,0 @@ -# News Sources Reference - -## Supported News Sources - -| Source ID | Name | Category | Description | -|:----------|:-----|:---------|:------------| -| `cls` | 财联社 | Finance | Real-time financial news, focus on A-shares and macro. | -| `wallstreetcn` | 华尔街见闻 | Finance | Global markets, macroeconomics, and detailed analysis. | -| `xueqiu` | 雪球热榜 | Finance | Community-driven stock discussions and hot topics. | -| `weibo` | 微博热搜 | General | Trending social topics, good for public sentiment. | -| `zhihu` | 知乎热榜 | General | In-depth discussions and Q&A on trending topics. | -| `baidu` | 百度热搜 | General | General public search trends. | -| `toutiao` | 今日头条 | General | Algorithmic news recommendations. | -| `douyin` | 抖音热榜 | General | Short video trends (titles only). | -| `thepaper` | 澎湃新闻 | General | Serious journalism and current affairs. | -| `36kr` | 36氪 | Tech | Startup, venture capital, and tech industry news. | -| `ithome` | IT之家 | Tech | Consumer electronics and tech gadgets. | -| `v2ex` | V2EX | Tech | Developer community trends. | -| `juejin` | 掘金 | Tech | Developer blogs and tutorials. | -| `hackernews` | Hacker News | Tech | Global tech and startup news (English). | - -## Polymarket - -- **Base URL**: `https://gamma-api.polymarket.com` -- **Data**: Prediction markets (e.g., "Will Fed cut rates?"). -- **Usage**: Use `get_active_markets` to retrieve top active markets by volume. diff --git a/skills/alphaear-news/scripts/__init__.py b/skills/alphaear-news/scripts/__init__.py deleted file mode 100644 index e69de29..0000000 diff --git a/skills/alphaear-news/scripts/content_extractor.py b/skills/alphaear-news/scripts/content_extractor.py deleted file mode 100644 index 133207a..0000000 --- a/skills/alphaear-news/scripts/content_extractor.py +++ /dev/null @@ -1,122 +0,0 @@ -import requests -from requests.exceptions import RequestException, Timeout, ConnectionError -import os -import time -import json -import threading -from typing import Optional -from loguru import logger - - -class ContentExtractor: - """内容提取工具 - 主要接入 Jina Reader API""" - - JINA_BASE_URL = "https://r.jina.ai/" - - # 速率限制配置 (无 API Key 时:20 次/分钟) - _rate_limit_no_key = 20 # 每分钟最大请求数 - _rate_window = 60.0 # 时间窗口(秒) - _min_interval = 3.0 # 请求最小间隔(秒) - - # 类级别的速率限制状态 - _request_times = [] - _last_request_time = 0.0 - _lock = threading.Lock() - - @classmethod - def _wait_for_rate_limit(cls, has_api_key: bool) -> None: - """等待以满足速率限制要求""" - if has_api_key: - # 有 API Key 时,只需保持最小间隔 - time.sleep(0.5) - return - - with cls._lock: - current_time = time.time() - - # 1. 清理过期的请求记录 - cls._request_times = [t for t in cls._request_times if current_time - t < cls._rate_window] - - # 2. 检查是否达到速率限制 - if len(cls._request_times) >= cls._rate_limit_no_key: - # 需要等待最旧的请求过期 - oldest = cls._request_times[0] - wait_time = cls._rate_window - (current_time - oldest) + 1.0 - if wait_time > 0: - logger.warning(f"⏳ Jina rate limit reached, waiting {wait_time:.1f}s...") - time.sleep(wait_time) - current_time = time.time() - cls._request_times = [t for t in cls._request_times if current_time - t < cls._rate_window] - - # 3. 确保请求间隔不太快 - time_since_last = current_time - cls._last_request_time - if time_since_last < cls._min_interval: - sleep_time = cls._min_interval - time_since_last - time.sleep(sleep_time) - - # 4. 记录本次请求 - cls._request_times.append(time.time()) - cls._last_request_time = time.time() - - @classmethod - def extract_with_jina(cls, url: str, timeout: int = 30) -> Optional[str]: - """ - 使用 Jina Reader 提取网页正文内容 (Markdown 格式) - - 无 API Key 时自动限速:每分钟最多 20 次请求,每次间隔至少 3 秒 - """ - if not url or not url.startswith("http"): - return None - - logger.info(f"🕸️ Extracting content from: {url} via Jina...") - - headers = { - "User-Agent": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/131.0.0.0 Safari/537.36", - "Accept": "application/json" - } - - # 使用统一的 JINA_API_KEY - api_key = os.getenv("JINA_API_KEY") - has_api_key = bool(api_key and api_key.strip()) - - if has_api_key: - headers["Authorization"] = f"Bearer {api_key}" - - # 等待速率限制 - cls._wait_for_rate_limit(has_api_key) - - try: - # Jina Reader API - full_url = f"{cls.JINA_BASE_URL}{url}" - response = requests.get(full_url, headers=headers, timeout=timeout) - - if response.status_code == 200: - try: - data = response.json() - # Jina JSON 响应格式通常在 data.content - if isinstance(data, dict) and "data" in data: - return data["data"].get("content", "") - return data.get("content", response.text) - except (json.JSONDecodeError, TypeError): - return response.text - elif response.status_code == 429: - # 触发速率限制,等待后重试一次 - logger.warning(f"⚠️ Jina rate limit (429), waiting 60s before retry...") - time.sleep(60) - return cls.extract_with_jina(url, timeout) - else: - logger.warning(f"Jina extraction failed (Status {response.status_code}) for {url}") - return None - - except Timeout: - logger.error(f"Timeout during Jina extraction for {url}") - return None - except ConnectionError: - logger.error(f"Connection error during Jina extraction for {url}") - return None - except RequestException as e: - logger.error(f"Request error during Jina extraction: {e}") - return None - except Exception as e: - logger.error(f"Unexpected error during Jina extraction: {e}") - return None diff --git a/skills/alphaear-news/scripts/database_manager.py b/skills/alphaear-news/scripts/database_manager.py deleted file mode 100644 index f5aa2a7..0000000 --- a/skills/alphaear-news/scripts/database_manager.py +++ /dev/null @@ -1,131 +0,0 @@ -import sqlite3 -import json -from datetime import datetime -from pathlib import Path -from typing import List, Dict, Optional -from loguru import logger - -class DatabaseManager: - """ - AlphaEar News Database Manager - Reduced version for alphaear-news skill - """ - - def __init__(self, db_path: str = "data/signal_flux.db"): - self.db_path = Path(db_path) - self.db_path.parent.mkdir(parents=True, exist_ok=True) - self.conn = sqlite3.connect(str(self.db_path), check_same_thread=False) - self.conn.row_factory = sqlite3.Row - self._init_db() - logger.debug(f"💾 Database initialized at {self.db_path}") - - def _init_db(self): - """Initialize news-related tables only""" - cursor = self.conn.cursor() - - # Daily News Table - cursor.execute(""" - CREATE TABLE IF NOT EXISTS daily_news ( - id TEXT PRIMARY KEY, - source TEXT, - rank INTEGER, - title TEXT, - url TEXT, - content TEXT, - publish_time TEXT, - crawl_time TEXT, - sentiment_score REAL, - analysis TEXT, - meta_data TEXT - ) - """) - - # Indexes - cursor.execute("CREATE INDEX IF NOT EXISTS idx_news_crawl_time ON daily_news(crawl_time)") - cursor.execute("CREATE INDEX IF NOT EXISTS idx_news_source ON daily_news(source)") - - self.conn.commit() - - # --- News Operations --- - - def save_daily_news(self, news_list: List[Dict]) -> int: - """Save hot news items""" - cursor = self.conn.cursor() - count = 0 - crawl_time = datetime.now().isoformat() - - for news in news_list: - try: - news_id = news.get('id') or f"{news.get('source')}_{news.get('rank')}_{crawl_time[:10]}" - cursor.execute(""" - INSERT OR REPLACE INTO daily_news - (id, source, rank, title, url, content, publish_time, crawl_time, sentiment_score, meta_data) - VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?) - """, ( - news_id, - news.get('source'), - news.get('rank'), - news.get('title'), - news.get('url'), - news.get('content', ''), - news.get('publish_time'), - crawl_time, - news.get('sentiment_score'), - json.dumps(news.get('meta_data', {})) - )) - count += 1 - except Exception as e: - logger.error(f"Error saving news item {news.get('title')}: {e}") - - self.conn.commit() - return count - - def get_daily_news(self, source: Optional[str] = None, limit: int = 100, days: int = 1) -> List[Dict]: - """Get recent news""" - cursor = self.conn.cursor() - time_threshold = (datetime.now().timestamp() - days * 86400) - time_threshold_str = datetime.fromtimestamp(time_threshold).isoformat() - - query = "SELECT * FROM daily_news WHERE crawl_time >= ?" - params = [time_threshold_str] - - if source: - query += " AND source = ?" - params.append(source) - - query += " ORDER BY crawl_time DESC, rank LIMIT ?" - params.append(limit) - - cursor.execute(query, params) - return [dict(row) for row in cursor.fetchall()] - - def delete_news(self, news_id: str) -> bool: - cursor = self.conn.cursor() - cursor.execute("DELETE FROM daily_news WHERE id = ?", (news_id,)) - self.conn.commit() - return cursor.rowcount > 0 - - def update_news_content(self, news_id: str, content: str = None, analysis: str = None) -> bool: - cursor = self.conn.cursor() - updates = [] - params = [] - - if content is not None: - updates.append("content = ?") - params.append(content) - if analysis is not None: - updates.append("analysis = ?") - params.append(analysis) - - if not updates: - return False - - params.append(news_id) - query = f"UPDATE daily_news SET {', '.join(updates)} WHERE id = ?" - cursor.execute(query, params) - self.conn.commit() - return cursor.rowcount > 0 - - def close(self): - if self.conn: - self.conn.close() diff --git a/skills/alphaear-news/scripts/news_tools.py b/skills/alphaear-news/scripts/news_tools.py deleted file mode 100644 index e833e2e..0000000 --- a/skills/alphaear-news/scripts/news_tools.py +++ /dev/null @@ -1,256 +0,0 @@ -import requests -from requests.exceptions import RequestException, Timeout -import json -import time -from datetime import datetime -from typing import List, Dict, Optional -from loguru import logger -from .database_manager import DatabaseManager -from .content_extractor import ContentExtractor - -class NewsNowTools: - """热点新闻获取工具 - 接入 NewsNow API 与 Jina 内容提取""" - - BASE_URL = "https://newsnow.busiyi.world" - SOURCES = { - # 金融类 - "cls": "财联社", - "wallstreetcn": "华尔街见闻", - "xueqiu": "雪球热榜", - # 综合/社交 - "weibo": "微博热搜", - "zhihu": "知乎热榜", - "baidu": "百度热搜", - "toutiao": "今日头条", - "douyin": "抖音热榜", - "thepaper": "澎湃新闻", - # 科技类 - "36kr": "36氪", - "ithome": "IT之家", - "v2ex": "V2EX", - "juejin": "掘金", - "hackernews": "Hacker News", - } - - - def __init__(self, db: DatabaseManager): - self.db = db - self.user_agent = ( - "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) " - "AppleWebKit/537.36 (KHTML, like Gecko) Chrome/131.0.0.0 Safari/537.36" - ) - self.extractor = ContentExtractor() - # Simple in-memory cache: source_id -> {"time": timestamp, "data": []} - self._cache = {} - - def fetch_hot_news(self, source_id: str, count: int = 15, fetch_content: bool = False) -> List[Dict]: - """ - 从指定新闻源获取热点新闻列表(支持5分钟缓存)。 - """ - # 1. Check cache validity (5 minutes) - cache_key = f"{source_id}_{count}" - cached = self._cache.get(cache_key) - now = time.time() - - if cached and (now - cached["time"] < 300): - logger.info(f"⚡ Using cached news for {source_id} (Age: {int(now - cached['time'])}s)") - return cached["data"] - - try: - url = f"{self.BASE_URL}/api/s?id={source_id}" - response = requests.get(url, headers={"User-Agent": self.user_agent}, timeout=30) - if response.status_code == 200: - data = response.json() - items = data.get("items", [])[:count] - processed_items = [] - for i, item in enumerate(items, 1): - item_url = item.get("url", "") - content = "" - if fetch_content and item_url: - content = self.extractor.extract_with_jina(item_url) or "" - - processed_items.append({ - "id": item.get("id") or f"{source_id}_{int(time.time())}_{i}", - "source": source_id, - "rank": i, - "title": item.get("title", ""), - "url": item_url, - "content": content, - "publish_time": item.get("publish_time"), - "meta_data": item.get("extra", {}) - }) - - # Update Cache - self._cache[cache_key] = {"time": now, "data": processed_items} - logger.info(f"✅ Fetched and cached news for {source_id}") - - self.db.save_daily_news(processed_items) - return processed_items - else: - logger.error(f"NewsNow API Error: {response.status_code}") - # Fallback to stale cache if available - if cached: - logger.warning(f"⚠️ API failed, using stale cache for {source_id}") - return cached["data"] - return [] - except Timeout: - logger.error(f"Timeout fetching hot news from {source_id}") - if cached: - logger.warning(f"⚠️ Timeout, using stale cache for {source_id}") - return cached["data"] - return [] - except RequestException as e: - logger.error(f"Network error fetching hot news from {source_id}: {e}") - if cached: - logger.warning(f"⚠️ Network check failed, using stale cache for {source_id}") - return cached["data"] - return [] - except json.JSONDecodeError: - logger.error(f"Failed to parse JSON response from NewsNow for {source_id}") - return [] - except Exception as e: - logger.error(f"Unexpected error fetching hot news from {source_id}: {e}") - return [] - - def fetch_news_content(self, url: str) -> Optional[str]: - """ - 使用 Jina Reader 抓取指定 URL 的网页正文内容。 - - Args: - url: 需要抓取内容的完整网页 URL,必须以 http:// 或 https:// 开头。 - - Returns: - 提取的网页正文内容 (Markdown 格式),如果失败则返回 None。 - """ - return self.extractor.extract_with_jina(url) - - def get_unified_trends(self, sources: Optional[List[str]] = None) -> str: - """ - 获取多平台综合热点报告,自动聚合多个新闻源的热门内容。 - - Args: - sources: 要扫描的新闻源列表。可选值按类别: - **金融类**: "cls", "wallstreetcn", "xueqiu" - **综合类**: "weibo", "zhihu", "baidu", "toutiao", "douyin", "thepaper" - **科技类**: "36kr", "ithome", "v2ex", "juejin", "hackernews" - - Returns: - 格式化的 Markdown 热点汇总报告,包含各平台 Top 10 热点标题和链接。 - """ - sources = sources or ["weibo", "zhihu", "wallstreetcn"] - all_news = [] - for src in sources: - all_news.extend(self.fetch_hot_news(src)) - time.sleep(0.2) - - if not all_news: - return "❌ 未能获取到热点数据" - - report = f"# 实时全网热点汇总 ({datetime.now().strftime('%Y-%m-%d %H:%M')})\n\n" - for src in sources: - - src_name = self.SOURCES.get(src, src) - report += f"### 🔥 {src_name}\n" - src_news = [n for n in all_news if n['source'] == src] - for n in src_news[:10]: - report += f"- {n['title']} ([链接]({n['url']}))\n" - report += "\n" - - return report - - -class PolymarketTools: - """Polymarket 预测市场数据工具 - 获取热门预测市场反映公众情绪和预期""" - - BASE_URL = "https://gamma-api.polymarket.com" - - def __init__(self, db: DatabaseManager): - self.db = db - self.user_agent = "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36" - - def get_active_markets(self, limit: int = 20) -> List[Dict]: - """ - 获取活跃的预测市场,用于分析公众情绪和预期。 - - 预测市场数据可以反映: - - 公众对重大事件的预期概率 - - 市场情绪和风险偏好 - - 热门话题的关注度 - - Args: - limit: 获取的市场数量,默认 20 个。 - - Returns: - 包含预测市场信息的列表,每个市场包含: - - question: 预测问题 - - outcomes: 可能的结果 - - outcomePrices: 各结果的概率价格 - - volume: 交易量 - """ - try: - response = requests.get( - f"{self.BASE_URL}/markets", - params={"active": "true", "closed": "false", "limit": limit}, - headers={"User-Agent": self.user_agent, "Accept": "application/json"}, - timeout=30 - ) - - if response.status_code == 200: - markets = response.json() - result = [] - for m in markets: - result.append({ - "id": m.get("id"), - "question": m.get("question"), - "slug": m.get("slug"), - "outcomes": m.get("outcomes"), - "outcomePrices": m.get("outcomePrices"), - "volume": m.get("volume"), - "liquidity": m.get("liquidity"), - }) - logger.info(f"✅ 获取 {len(result)} 个预测市场") - return result - else: - logger.warning(f"⚠️ Polymarket API 返回 {response.status_code}") - return [] - except Timeout: - logger.error("Timeout fetching Polymarket markets") - return [] - except RequestException as e: - logger.error(f"Network error fetching Polymarket markets: {e}") - return [] - except json.JSONDecodeError: - logger.error("Failed to parse JSON response from Polymarket") - return [] - except Exception as e: - logger.error(f"Unexpected error fetching Polymarket markets: {e}") - return [] - - def get_market_summary(self, limit: int = 10) -> str: - """ - 获取预测市场摘要报告,用于了解当前热门话题和公众预期。 - - Args: - limit: 获取的市场数量 - - Returns: - 格式化的预测市场报告 - """ - markets = self.get_active_markets(limit) - if not markets: - return "❌ 无法获取 Polymarket 数据" - - report = f"# 🔮 Polymarket 热门预测 ({datetime.now().strftime('%Y-%m-%d %H:%M')})\n\n" - for i, m in enumerate(markets, 1): - question = m.get("question", "Unknown") - prices = m.get("outcomePrices", []) - volume = m.get("volume", 0) - - report += f"**{i}. {question}**\n" - if prices: - report += f" 概率: {prices}\n" - if volume: - report += f" 交易量: ${float(volume):,.0f}\n" - report += "\n" - - return report diff --git a/skills/alphaear-news/tests/test_news.py b/skills/alphaear-news/tests/test_news.py deleted file mode 100644 index 9f5ce1c..0000000 --- a/skills/alphaear-news/tests/test_news.py +++ /dev/null @@ -1,24 +0,0 @@ -import sys -import os -import unittest - -# Add skill root to path -sys.path.append(os.path.abspath(os.path.join(os.path.dirname(__file__), '..'))) - -try: - from scripts.news_tools import NewsNowTools - from scripts.database_manager import DatabaseManager -except ImportError as e: - print(f"Import Error: {e}") - sys.exit(1) - -class TestNews(unittest.TestCase): - def test_init(self): - print("Testing NewsNowTools Iteration...") - db = DatabaseManager(":memory:") - tools = NewsNowTools(db) - self.assertIsNotNone(tools) - print("NewsNowTools Initialized.") - -if __name__ == '__main__': - unittest.main() diff --git a/skills/alphaear-predictor/SKILL.md b/skills/alphaear-predictor/SKILL.md deleted file mode 100644 index 95aabf7..0000000 --- a/skills/alphaear-predictor/SKILL.md +++ /dev/null @@ -1,60 +0,0 @@ ---- -name: alphaear-predictor -description: Market prediction skill using Kronos. Use when user needs finance market time-series forecasting or news-aware finance market adjustments. ---- - -# AlphaEar Predictor Skill - -## Overview - -This skill utilizes the Kronos model (via `KronosPredictorUtility`) to perform time-series forecasting and adjust predictions based on news sentiment. - -## Capabilities - -### 1. Forecast Market Trends - -### 1. Forecast Market Trends - -**Workflow:** -1. **Generate Base Forecast**: Use `scripts/kronos_predictor.py` (via `KronosPredictorUtility`) to generate the technical/quantitative forecast. -2. **Adjust Forecast (Agentic)**: Use the **Forecast Adjustment Prompt** in `references/PROMPTS.md` to subjectively adjust the numbers based on latest news/logic. - -**Key Tools:** -- `KronosPredictorUtility.get_base_forecast(df, lookback, pred_len, news_text)`: Returns `List[KLinePoint]`. - -**Example Usage (Python):** - -```python -from scripts.utils.kronos_predictor import KronosPredictorUtility -from scripts.utils.database_manager import DatabaseManager - -db = DatabaseManager() -predictor = KronosPredictorUtility() - -# Forecast -forecast = predictor.predict("600519", horizon="7d") -print(forecast) -``` - - -## Configuration - -This skill requires the **Kronos** model and an embedding model. - -1. **Kronos Model**: - - Ensure `exports/models` directory exists in the project root. - - Place trained news projector weights (e.g., `kronos_news_v1.pt`) in `exports/models/`. - - Or depend on the base model (automatically downloaded). - -2. **Environment Variables**: - - `EMBEDDING_MODEL`: Path or name of the embedding model (default: `sentence-transformers/all-MiniLM-L6-v2`). - - `KRONOS_MODEL_PATH`: Optional path to override model loading. - -## Dependencies - -- `torch` -- `transformers` -- `sentence-transformers` -- `pandas` -- `numpy` -- `scikit-learn` diff --git a/skills/alphaear-predictor/references/PROMPTS.md b/skills/alphaear-predictor/references/PROMPTS.md deleted file mode 100644 index 02fe9c5..0000000 --- a/skills/alphaear-predictor/references/PROMPTS.md +++ /dev/null @@ -1,43 +0,0 @@ -# AlphaEar Predictor Prompts - -## Forecast Adjustment (Analyst) - -**Prompt:** - -```markdown -You are a senior quantitative strategy analyst. -Your task is to subjectively/logically adjust the given [Kronos Model Forecast] based on the [Latest Intelligence/News Context]. - -Ticker: {ticker} - -【Kronos Base Forecast (OHLC)】: -{forecast_str} - -【Latest Intelligence Context】: -{news_context} - -**Adjustment Principles:** -1. Base forecast is technical-only. -2. Context may contain a "Quantitative Correction" from a news-aware model. **Highly respect** this unless logic is flawed. -3. Use qualitative analysis (news logic) to verify or fine-tune. -4. If no quantitative correction exists, verify trend manually against news sentiment. - -**Output (Strict JSON):** -```json -{ - "adjusted_forecast": [ - { - "date": "YYYY-MM-DD", - "open": , - "high": , - "low": , - "close": , - "volume": - }, - ... - ], - "rationale": "Detailed logic..." -} -``` -Ensure same number of data points as base forecast. -``` diff --git a/skills/alphaear-predictor/scripts/__init__.py b/skills/alphaear-predictor/scripts/__init__.py deleted file mode 100644 index e69de29..0000000 diff --git a/skills/alphaear-predictor/scripts/forecast_agent.py b/skills/alphaear-predictor/scripts/forecast_agent.py deleted file mode 100644 index 4bbf67e..0000000 --- a/skills/alphaear-predictor/scripts/forecast_agent.py +++ /dev/null @@ -1,76 +0,0 @@ -import json -from typing import List, Optional, Dict, Any -from datetime import datetime -from loguru import logger -import pandas as pd - -from .kronos_predictor import KronosPredictorUtility -from .utils.database_manager import DatabaseManager -from .schema.models import ForecastResult, KLinePoint, InvestmentSignal - -class ForecastUtils: - """ - 预测辅助工具 (ForecastUtils) - 提供数据准备、基础模型预测等功能。 - LLM 调整逻辑已移交 Agent 执行 (参考 scripts/prompts/PROMPTS.md)。 - """ - - def __init__(self, db: DatabaseManager): - self.db = db - self.predictor_util = KronosPredictorUtility() # Singleton - - def get_base_forecast( - self, - ticker: str, - signals: List[Dict] = None, - lookback: int = 20, - pred_len: int = 5, - ) -> Optional[List[KLinePoint]]: - """ - 获取基础预测数据 (技术面 + 新闻模型定量修正)。 - Agent 应随后使用 PROMPTS.md 中的指令进行定性调整。 - """ - logger.info(f"🔮 Generating base forecast for {ticker}...") - - # 1. 获取历史数据 - from .stock_tools import StockTools - stock_tools = StockTools(self.db, auto_update=False) - - end_date = datetime.now().strftime("%Y-%m-%d") - # 宽放一点时间以确保有足够的交易日 - start_date = (datetime.now() - pd.Timedelta(days=max(lookback * 4, 90))).strftime("%Y-%m-%d") - df = stock_tools.get_stock_price(ticker, start_date=start_date, end_date=end_date) - - if df.empty or len(df) < lookback: - # Try force sync - df = stock_tools.get_stock_price(ticker, start_date=start_date, end_date=end_date, force_sync=True) - - if df.empty: - logger.warning(f"⚠️ No history data for {ticker}") - return None - - effective_lookback = lookback - if len(df) < lookback: - if len(df) < 10: - logger.warning(f"⚠️ Insufficient history for {ticker}") - return None - effective_lookback = len(df) - - # 2. 准备信号上下文 - signal_lines = [] - for s in (signals or []): - try: - title = s.get('title', '') if isinstance(s, dict) else getattr(s, 'title', '') - summary = s.get('summary', '') if isinstance(s, dict) else getattr(s, 'summary', '') - if title or summary: - signal_lines.append(f"- {title}: {summary}") - except Exception: - continue - - signals_context = "\n".join(signal_lines).strip() - - # 3. 模型预测 (News-Adjusted if context exists) - if signals_context: - return self.predictor_util.get_base_forecast(df, lookback=effective_lookback, pred_len=pred_len, news_text=signals_context) - else: - return self.predictor_util.get_base_forecast(df, lookback=effective_lookback, pred_len=pred_len, news_text=None) diff --git a/skills/alphaear-predictor/scripts/json_utils.py b/skills/alphaear-predictor/scripts/json_utils.py deleted file mode 100644 index c29aab2..0000000 --- a/skills/alphaear-predictor/scripts/json_utils.py +++ /dev/null @@ -1,180 +0,0 @@ -import ast -import json -import re -from typing import Optional, Any -from loguru import logger - -def _strip_comments(text: str) -> str: - """ - Safely remove C-style comments (// and /* */) from JSON-like text, - preserving strings (including URLs like http://). - """ - result = [] - i = 0 - n = len(text) - in_string = False - escape = False - - while i < n: - char = text[i] - - if in_string: - if char == '\\': - escape = not escape - elif char == '"' and not escape: - in_string = False - else: - escape = False - result.append(char) - i += 1 - continue - - # Not in string - if char == '"': - in_string = True - result.append(char) - i += 1 - continue - - # Check for // comment - if i + 1 < n and text[i:i+2] == '//': - i += 2 - while i < n and text[i] != '\n': - i += 1 - continue - - # Check for /* comment - if i + 1 < n and text[i:i+2] == '/*': - i += 2 - while i + 1 < n and text[i:i+2] != '*/': - i += 1 - i += 2 - continue - - result.append(char) - i += 1 - - return ''.join(result) - -def extract_json(text: str) -> Optional[Any]: - """ - 更加鲁棒的 JSON 提取工具。 - 处理: - 1. Markdown 代码块 (```json ... ```) - 2. 首尾多余字符 - 3. 同一个文本中多个 JSON 对象 (仅提取第一个) - 4. 简单的 JSON 修复 (末尾逗号等) - 5. C 风格注释 (// 和 /* */) - """ - if not text: - return None - - # 1. 清理明显的 Markdown 包装 - text = text.strip() - - # 先尝试精确匹配 ```json ... ``` 或 ```...``` - md_match = re.search(r'```(?:json)?\s*\n?(.*?)\n?```', text, re.DOTALL) - if md_match: - text = md_match.group(1).strip() - elif text.startswith("```"): - # 回退:如果开头有 ``` 但没完整匹配 - text = re.sub(r'^```[a-z]*\n?', '', text) - text = re.sub(r'\n?```\s*$', '', text) - - # 2. 寻找第一个 JSON 起始符 { 或 [ - start_brace = text.find('{') - start_bracket = text.find('[') - - if start_brace == -1 and start_bracket == -1: - return None - - start_idx = start_brace if (start_bracket == -1 or (start_brace != -1 and start_brace < start_bracket)) else start_bracket - - # 2.5 预处理:修复一些极其常见的 LLM 错误 - potential_json = text[start_idx:].strip() - - # remove comments safely - potential_json = _strip_comments(potential_json) - - # b. 修复缺失开头引号的键: nodes": [ -> "nodes": [ - # 匹配模式: (空白或换行) 单词 紧跟引号和冒号 - potential_json = re.sub(r'([\{\,]\s*)([a-zA-Z_]\w*)\"\s*:', r'\1"\2":', potential_json) - - # c. 修复缺失末尾引号的键: "nodes: [ -> "nodes": [ - potential_json = re.sub(r'([\{\,]\s*)\"([a-zA-Z_]\w*)\s*:', r'\1"\2":', potential_json) - - # d. 修复完全缺失引号的键: nodes: [ -> "nodes": [ - # 注意避免匹配到像 http:// 这种内容,所以限定在 { 或 , 之后 - potential_json = re.sub(r'([\{\,]\s*)([a-zA-Z_]\w*)\s*:', r'\1"\2":', potential_json) - - # 3. 使用 raw_decode 尝试解析 - decoder = json.JSONDecoder() - - # 首先尝试直接解析(不做任何预处理) - try: - obj = json.loads(potential_json) - return obj - except json.JSONDecodeError: - pass - - # 简单预处理:移除对象/列表末位多余逗号 - processed_json = re.sub(r',\s*([\]}])', r'\1', potential_json) - - try: - obj, end_pos = decoder.raw_decode(processed_json) - return obj - except json.JSONDecodeError: - pass - - # e. 修复未终止的字符串字面量问题:移除值中的实际换行符 - # LLM 可能在字符串值中生成包含真实 newline 的内容,导致 JSON 非法 - def fix_multiline_strings(s): - # 简单策略:将字符串值内的换行替换为空格 - lines = s.split('\n') - result = [] - in_string = False - for line in lines: - # 计算未转义的引号数 - quote_count = line.count('"') - line.count('\\"') - if in_string: - result[-1] += ' ' + line.strip() - else: - result.append(line) - - if quote_count % 2 == 1: - in_string = not in_string - return '\n'.join(result) - - fixed_json = fix_multiline_strings(processed_json) - - try: - obj, end_pos = decoder.raw_decode(fixed_json) - return obj - except json.JSONDecodeError: - try: - # 4. 尝试处理单引号问题 (JSON 规范要求双引号,但 LLM 常输出单引号) - # 这是一个简单的替换技巧,仅针对像 {'key': 'value'} 这样的结构 - # 注意:这可能会破坏包含单引号的字符串值,所以作为较后的回退 - fix_quotes = re.sub(r"'(.*?)':", r'"\1":', processed_json) # 修复键 - fix_quotes = re.sub(r":\s*'(.*?)'", r': "\1"', fix_quotes) # 修复简单值 - obj, end_pos = decoder.raw_decode(fix_quotes) - return obj - except (json.JSONDecodeError, TypeError): - try: - # 5. 使用 ast.literal_eval 作为终极回退 (处理 Python 字典格式) - # 提取第一个匹配的括号对内容 - # 寻找匹配的 { } - stack = [] - for i, char in enumerate(potential_json): - if char == '{': stack.append('{') - elif char == '}': - if stack: stack.pop() - if not stack: - content = potential_json[:i+1] - return ast.literal_eval(content) - except (ValueError, SyntaxError, MemoryError) as e: - logger.warning(f"All JSON extraction attempts failed: {e}") - except Exception as e: - logger.error(f"Unexpected error during JSON extraction: {e}") - - return None diff --git a/skills/alphaear-predictor/scripts/kronos_predictor.py b/skills/alphaear-predictor/scripts/kronos_predictor.py deleted file mode 100644 index b46ee6e..0000000 --- a/skills/alphaear-predictor/scripts/kronos_predictor.py +++ /dev/null @@ -1,218 +0,0 @@ -import torch -import pandas as pd -import numpy as np -from datetime import datetime -from typing import List, Optional -from loguru import logger -from pandas.tseries.offsets import BusinessDay -import os -import sys - -KRONOS_DIR = os.path.join(os.path.dirname(__file__), "predictor") -if KRONOS_DIR not in sys.path: - sys.path.append(KRONOS_DIR) - -from skills._env_loader import load_unified_env - -load_unified_env() - -import glob -from sentence_transformers import SentenceTransformer - -from .predictor.model import Kronos, KronosTokenizer, KronosPredictor -from .schema.models import KLinePoint - - -class KronosPredictorUtility: - """ - Kronos 时序预测工具类 - 负责模型加载、推理以及数据结构转换 - """ - - _instance = None - _predictor = None - - def __new__(cls, *args, **kwargs): - if not cls._instance: - cls._instance = super(KronosPredictorUtility, cls).__new__(cls) - return cls._instance - - def __init__(self, device: Optional[str] = None): - if self._predictor is not None: - return - - try: - if not device: - device = ( - "cuda" - if torch.cuda.is_available() - else "mps" - if torch.backends.mps.is_available() - else "cpu" - ) - - logger.info(f"🔮 Loading Kronos Model on {device}...") - - # 1. Load Embedder (SentenceTransformer) - model_name = os.getenv( - "EMBEDDING_MODEL", "sentence-transformers/all-MiniLM-L6-v2" - ) # Match training - try: - self.embedder = SentenceTransformer( - model_name, device=device, local_files_only=True - ) - except Exception: - logger.warning( - f"⚠️ Local embedder {model_name} not found. Downloading..." - ) - self.embedder = SentenceTransformer(model_name, device=device) - - # 2. Load Kronos Base - try: - tokenizer = KronosTokenizer.from_pretrained( - "NeoQuasar/Kronos-Tokenizer-base", local_files_only=True - ) - model = Kronos.from_pretrained( - "NeoQuasar/Kronos-base", local_files_only=True - ) - except Exception: - logger.warning( - "⚠️ Local Kronos cache not found. Attempting to download..." - ) - tokenizer = KronosTokenizer.from_pretrained( - "NeoQuasar/Kronos-Tokenizer-base" - ) - model = Kronos.from_pretrained("NeoQuasar/Kronos-base") - - # 3. Load Trained News Projector Weights - # Check predictor/exports/models directory - models_dir = os.path.join(KRONOS_DIR, "exports/models") - model_files = glob.glob(os.path.join(models_dir, "*.pt")) - - if model_files: - latest_model = max(model_files, key=os.path.getctime) - logger.info(f"🔄 Loading trained news weights from {latest_model}...") - try: - checkpoint = torch.load(latest_model, map_location=device) - # The checkpoint contains 'news_proj_state_dict' - if "news_proj_state_dict" in checkpoint: - if not hasattr(model, "news_proj") or model.news_proj is None: - import torch.nn as nn - - news_dim = checkpoint.get("news_dim", 384) - model.news_proj = nn.Linear(news_dim, model.d_model).to( - device - ) - - model.news_proj.load_state_dict( - checkpoint["news_proj_state_dict"] - ) - logger.success("✅ News-Aware Projection Layer loaded!") - self.has_news_model = True - else: - logger.warning( - "⚠️ Checkpoint found but missing 'news_proj_state_dict'. Using base model." - ) - self.has_news_model = False - except Exception as e: - logger.error( - f"❌ Failed to load trained weights: {e}. Using base model." - ) - self.has_news_model = False - else: - logger.info("ℹ️ No trained news models found. Using base model.") - self.has_news_model = False - - tokenizer = tokenizer.to(device) - model = model.to(device) - - self._predictor = KronosPredictor( - model, tokenizer, device=device, max_context=512 - ) - logger.info("✅ Kronos Model loaded successfully.") - except Exception as e: - logger.error(f"❌ Failed to load Kronos Model: {e}") - self._predictor = None - self.has_news_model = False - - def get_base_forecast( - self, - df: pd.DataFrame, - lookback: int = 20, - pred_len: int = 5, - news_text: Optional[str] = None, - ) -> List[KLinePoint]: - """ - 生成原始模型预测 - """ - if self._predictor is None: - logger.error("Predictor not initialized.") - return [] - - if len(df) < lookback: - logger.warning( - f"Insufficient historical data ({len(df)}) for lookback ({lookback})." - ) - return [] - - # 获取最后 lookback 条数据 - x_df = df.iloc[-lookback:].copy() - x_timestamp = pd.to_datetime(x_df["date"]) # Ensure datetime - last_date = x_timestamp.iloc[-1] - - # 生成未来时间戳 - future_dates = pd.date_range( - start=last_date + BusinessDay(1), periods=pred_len, freq="B" - ) - y_timestamp = pd.Series(future_dates) - - # Embedding News if available - news_emb = None - if ( - news_text - and getattr(self, "has_news_model", False) - and hasattr(self, "embedder") - ): - try: - # Truncate to avoid too long text - emb = self.embedder.encode(news_text[:1000]) - news_emb = emb # KronosPredictor expects numpy array or tensor - except Exception as e: - logger.error(f"Failed to encode news: {e}") - - try: - # 预测所需的列 - cols = ["open", "high", "low", "close", "volume"] - pred_df = self._predictor.predict( - df=x_df[cols], - x_timestamp=x_timestamp, - y_timestamp=y_timestamp, - pred_len=pred_len, - T=1.0, - top_p=0.9, - sample_count=1, - verbose=False, - news_emb=news_emb, - ) - - # 转换为 KLinePoint - results = [] - for date, row in pred_df.iterrows(): - results.append( - KLinePoint( - date=date.strftime("%Y-%m-%d"), - open=float(row["open"]), - high=float(row["high"]), - low=float(row["low"]), - close=float(row["close"]), - volume=float(row["volume"]), - ) - ) - return results - except Exception as e: - logger.error(f"Forecast generation failed: {e}") - return [] - - -# Singleton instance for easy access -# Usage: predictor = KronosPredictorUtility() diff --git a/skills/alphaear-predictor/scripts/predictor/exports/models/kronos_news_v1_20260101_0015.pt b/skills/alphaear-predictor/scripts/predictor/exports/models/kronos_news_v1_20260101_0015.pt deleted file mode 100644 index 097a60bf897c9a5c12bd696fc1cf3ceef5ce6ca6..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 1283533 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 z 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

=-s<@7_+BX9 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

KbQ6q~gz}n>1?G7s%Y3Kr7!n@kcEc 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

HC&j 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 pZ!MGS-~9Y_Cwkk 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�}+5LfD$_kQn8R&OIAovb`1m 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 diff --git a/skills/alphaear-predictor/scripts/predictor/model/__init__.py b/skills/alphaear-predictor/scripts/predictor/model/__init__.py deleted file mode 100644 index d10e200..0000000 --- a/skills/alphaear-predictor/scripts/predictor/model/__init__.py +++ /dev/null @@ -1,16 +0,0 @@ -from .kronos import KronosTokenizer, Kronos, KronosPredictor - -model_dict = { - 'kronos_tokenizer': KronosTokenizer, - 'kronos': Kronos, - 'kronos_predictor': KronosPredictor -} - - -def get_model_class(model_name): - if model_name in model_dict: - return model_dict[model_name] - else: - print(f"Model {model_name} not found in model_dict") - raise NotImplementedError - diff --git a/skills/alphaear-predictor/scripts/predictor/model/kronos.py b/skills/alphaear-predictor/scripts/predictor/model/kronos.py deleted file mode 100644 index cf8bece..0000000 --- a/skills/alphaear-predictor/scripts/predictor/model/kronos.py +++ /dev/null @@ -1,676 +0,0 @@ -import numpy as np -import pandas as pd -import torch -from huggingface_hub import PyTorchModelHubMixin -import sys - -from tqdm import trange - -sys.path.append("../") -from model.module import * - - -class KronosTokenizer(nn.Module, PyTorchModelHubMixin): - """ - KronosTokenizer module for tokenizing input data using a hybrid quantization approach. - - This tokenizer utilizes a combination of encoder and decoder Transformer blocks - along with the Binary Spherical Quantization (BSQuantizer) to compress and decompress input data. - - Args: - d_in (int): Input dimension. - d_model (int): Model dimension. - n_heads (int): Number of attention heads. - ff_dim (int): Feed-forward dimension. - n_enc_layers (int): Number of encoder layers. - n_dec_layers (int): Number of decoder layers. - ffn_dropout_p (float): Dropout probability for feed-forward networks. - attn_dropout_p (float): Dropout probability for attention mechanisms. - resid_dropout_p (float): Dropout probability for residual connections. - s1_bits (int): Number of bits for the pre token in BSQuantizer. - s2_bits (int): Number of bits for the post token in BSQuantizer. - beta (float): Beta parameter for BSQuantizer. - gamma0 (float): Gamma0 parameter for BSQuantizer. - gamma (float): Gamma parameter for BSQuantizer. - zeta (float): Zeta parameter for BSQuantizer. - group_size (int): Group size parameter for BSQuantizer. - - """ - - def __init__(self, d_in, d_model, n_heads, ff_dim, n_enc_layers, n_dec_layers, ffn_dropout_p, attn_dropout_p, resid_dropout_p, s1_bits, s2_bits, beta, gamma0, gamma, zeta, group_size): - - super().__init__() - self.d_in = d_in - self.d_model = d_model - self.n_heads = n_heads - self.ff_dim = ff_dim - self.enc_layers = n_enc_layers - self.dec_layers = n_dec_layers - self.ffn_dropout_p = ffn_dropout_p - self.attn_dropout_p = attn_dropout_p - self.resid_dropout_p = resid_dropout_p - - self.s1_bits = s1_bits - self.s2_bits = s2_bits - self.codebook_dim = s1_bits + s2_bits # Total dimension of the codebook after quantization - self.embed = nn.Linear(self.d_in, self.d_model) - self.head = nn.Linear(self.d_model, self.d_in) - - # Encoder Transformer Blocks - self.encoder = nn.ModuleList([ - TransformerBlock(self.d_model, self.n_heads, self.ff_dim, self.ffn_dropout_p, self.attn_dropout_p, self.resid_dropout_p) - for _ in range(self.enc_layers - 1) - ]) - # Decoder Transformer Blocks - self.decoder = nn.ModuleList([ - TransformerBlock(self.d_model, self.n_heads, self.ff_dim, self.ffn_dropout_p, self.attn_dropout_p, self.resid_dropout_p) - for _ in range(self.dec_layers - 1) - ]) - self.quant_embed = nn.Linear(in_features=self.d_model, out_features=self.codebook_dim) # Linear layer before quantization - self.post_quant_embed_pre = nn.Linear(in_features=self.s1_bits, out_features=self.d_model) # Linear layer after quantization (pre part - s1 bits) - self.post_quant_embed = nn.Linear(in_features=self.codebook_dim, out_features=self.d_model) # Linear layer after quantization (full codebook) - self.tokenizer = BSQuantizer(self.s1_bits, self.s2_bits, beta, gamma0, gamma, zeta, group_size) # BSQuantizer module - - def forward(self, x): - """ - Forward pass of the KronosTokenizer. - - Args: - x (torch.Tensor): Input tensor of shape (batch_size, seq_len, d_in). - - Returns: - tuple: A tuple containing: - - tuple: (z_pre, z) - Reconstructed outputs from decoder with s1_bits and full codebook respectively, - both of shape (batch_size, seq_len, d_in). - - torch.Tensor: bsq_loss - Loss from the BSQuantizer. - - torch.Tensor: quantized - Quantized representation from BSQuantizer. - - torch.Tensor: z_indices - Indices from the BSQuantizer. - """ - z = self.embed(x) - - for layer in self.encoder: - z = layer(z) - - z = self.quant_embed(z) # (B, T, codebook) - - bsq_loss, quantized, z_indices = self.tokenizer(z) - - quantized_pre = quantized[:, :, :self.s1_bits] # Extract the first part of quantized representation (s1_bits) - z_pre = self.post_quant_embed_pre(quantized_pre) - - z = self.post_quant_embed(quantized) - - # Decoder layers (for pre part - s1 bits) - for layer in self.decoder: - z_pre = layer(z_pre) - z_pre = self.head(z_pre) - - # Decoder layers (for full codebook) - for layer in self.decoder: - z = layer(z) - z = self.head(z) - - return (z_pre, z), bsq_loss, quantized, z_indices - - def indices_to_bits(self, x, half=False): - """ - Converts indices to bit representations and scales them. - - Args: - x (torch.Tensor): Indices tensor. - half (bool, optional): Whether to process only half of the codebook dimension. Defaults to False. - - Returns: - torch.Tensor: Bit representation tensor. - """ - if half: - x1 = x[0] # Assuming x is a tuple of indices if half is True - x2 = x[1] - mask = 2 ** torch.arange(self.codebook_dim//2, device=x1.device, dtype=torch.long) # Create a mask for bit extraction - x1 = (x1.unsqueeze(-1) & mask) != 0 # Extract bits for the first half - x2 = (x2.unsqueeze(-1) & mask) != 0 # Extract bits for the second half - x = torch.cat([x1, x2], dim=-1) # Concatenate the bit representations - else: - mask = 2 ** torch.arange(self.codebook_dim, device=x.device, dtype=torch.long) # Create a mask for bit extraction - x = (x.unsqueeze(-1) & mask) != 0 # Extract bits - - x = x.float() * 2 - 1 # Convert boolean to bipolar (-1, 1) - q_scale = 1. / (self.codebook_dim ** 0.5) # Scaling factor - x = x * q_scale - return x - - def encode(self, x, half=False): - """ - Encodes the input data into quantized indices. - - Args: - x (torch.Tensor): Input tensor of shape (batch_size, seq_len, d_in). - half (bool, optional): Whether to use half quantization in BSQuantizer. Defaults to False. - - Returns: - torch.Tensor: Quantized indices from BSQuantizer. - """ - z = self.embed(x) - for layer in self.encoder: - z = layer(z) - z = self.quant_embed(z) - - bsq_loss, quantized, z_indices = self.tokenizer(z, half=half, collect_metrics=False) - return z_indices - - def decode(self, x, half=False): - """ - Decodes quantized indices back to the input data space. - - Args: - x (torch.Tensor): Quantized indices tensor. - half (bool, optional): Whether the indices were generated with half quantization. Defaults to False. - - Returns: - torch.Tensor: Reconstructed output tensor of shape (batch_size, seq_len, d_in). - """ - quantized = self.indices_to_bits(x, half) - z = self.post_quant_embed(quantized) - for layer in self.decoder: - z = layer(z) - z = self.head(z) - return z - - -class Kronos(nn.Module, PyTorchModelHubMixin): - """ - Kronos Model. - - Args: - s1_bits (int): Number of bits for pre tokens. - s2_bits (int): Number of bits for post tokens. - n_layers (int): Number of Transformer blocks. - d_model (int): Dimension of the model's embeddings and hidden states. - n_heads (int): Number of attention heads in the MultiheadAttention layers. - ff_dim (int): Dimension of the feedforward network in the Transformer blocks. - ffn_dropout_p (float): Dropout probability for the feedforward network. - attn_dropout_p (float): Dropout probability for the attention layers. - resid_dropout_p (float): Dropout probability for residual connections. - token_dropout_p (float): Dropout probability for token embeddings. - learn_te (bool): Whether to use learnable temporal embeddings. - """ - - def __init__(self, s1_bits, s2_bits, n_layers, d_model, n_heads, ff_dim, ffn_dropout_p, attn_dropout_p, resid_dropout_p, token_dropout_p, learn_te, news_dim=None): - super().__init__() - self.s1_bits = s1_bits - self.s2_bits = s2_bits - self.n_layers = n_layers - self.d_model = d_model - self.n_heads = n_heads - self.learn_te = learn_te - self.ff_dim = ff_dim - self.ffn_dropout_p = ffn_dropout_p - self.attn_dropout_p = attn_dropout_p - self.resid_dropout_p = resid_dropout_p - self.token_dropout_p = token_dropout_p - self.news_dim = news_dim - - self.s1_vocab_size = 2 ** self.s1_bits - self.token_drop = nn.Dropout(self.token_dropout_p) - self.embedding = HierarchicalEmbedding(self.s1_bits, self.s2_bits, self.d_model) - self.time_emb = TemporalEmbedding(self.d_model, self.learn_te) - self.transformer = nn.ModuleList([ - TransformerBlock(self.d_model, self.n_heads, self.ff_dim, self.ffn_dropout_p, self.attn_dropout_p, self.resid_dropout_p) - for _ in range(self.n_layers) - ]) - self.norm = RMSNorm(self.d_model) - self.dep_layer = DependencyAwareLayer(self.d_model) - self.head = DualHead(self.s1_bits, self.s2_bits, self.d_model) - - if self.news_dim is not None: - self.news_proj = nn.Linear(self.news_dim, self.d_model) - else: - self.news_proj = None - - self.apply(self._init_weights) - - def _init_weights(self, module): - - if isinstance(module, nn.Linear): - nn.init.xavier_normal_(module.weight) - if module.bias is not None: - nn.init.zeros_(module.bias) - elif isinstance(module, nn.Embedding): - nn.init.normal_(module.weight, mean=0, std=self.embedding.d_model ** -0.5) - elif isinstance(module, nn.LayerNorm): - nn.init.ones_(module.weight) - nn.init.zeros_(module.bias) - elif isinstance(module, RMSNorm): - nn.init.ones_(module.weight) - - def forward(self, s1_ids, s2_ids, stamp=None, padding_mask=None, use_teacher_forcing=False, s1_targets=None, news_emb=None): - """ - Args: - s1_ids (torch.Tensor): Input tensor of s1 token IDs. Shape: [batch_size, seq_len] - s2_ids (torch.Tensor): Input tensor of s2 token IDs. Shape: [batch_size, seq_len] - stamp (torch.Tensor, optional): Temporal stamp tensor. Shape: [batch_size, seq_len]. Defaults to None. - padding_mask (torch.Tensor, optional): Mask for padding tokens. Shape: [batch_size, seq_len]. Defaults to None. - use_teacher_forcing (bool, optional): Whether to use teacher forcing for s1 decoding. Defaults to False. - s1_targets (torch.Tensor, optional): Target s1 token IDs for teacher forcing. Shape: [batch_size, seq_len]. Defaults to None. - news_emb (torch.Tensor, optional): News embedding tensor. Shape: [batch_size, news_dim]. Defaults to None. - - Returns: - Tuple[torch.Tensor, torch.Tensor]: - - s1 logits: Logits for s1 token predictions. Shape: [batch_size, seq_len, s1_vocab_size] - - s2_logits: Logits for s2 token predictions, conditioned on s1. Shape: [batch_size, seq_len, s2_vocab_size] - """ - x = self.embedding([s1_ids, s2_ids]) - if stamp is not None: - time_embedding = self.time_emb(stamp) - x = x + time_embedding - x = self.token_drop(x) - - for layer in self.transformer: - x = layer(x, key_padding_mask=padding_mask) - - x = self.norm(x) - - if news_emb is not None and self.news_proj is not None: - news_bias = self.news_proj(news_emb).unsqueeze(1) # [B, 1, d_model] - x = x + news_bias - - s1_logits = self.head(x) - - if use_teacher_forcing: - sibling_embed = self.embedding.emb_s1(s1_targets) - else: - s1_probs = F.softmax(s1_logits.detach(), dim=-1) - sample_s1_ids = torch.multinomial(s1_probs.view(-1, self.s1_vocab_size), 1).view(s1_ids.shape) - sibling_embed = self.embedding.emb_s1(sample_s1_ids) - - x2 = self.dep_layer(x, sibling_embed, key_padding_mask=padding_mask) # Dependency Aware Layer: Condition on s1 embeddings - s2_logits = self.head.cond_forward(x2) - return s1_logits, s2_logits - - def decode_s1(self, s1_ids, s2_ids, stamp=None, padding_mask=None, news_emb=None): - """ - Decodes only the s1 tokens. - - This method performs a forward pass to predict only s1 tokens. It returns the s1 logits - and the context representation from the Transformer, which can be used for subsequent s2 decoding. - - Args: - s1_ids (torch.Tensor): Input tensor of s1 token IDs. Shape: [batch_size, seq_len] - s2_ids (torch.Tensor): Input tensor of s2 token IDs. Shape: [batch_size, seq_len] - stamp (torch.Tensor, optional): Temporal stamp tensor. Shape: [batch_size, seq_len]. Defaults to None. - padding_mask (torch.Tensor, optional): Mask for padding tokens. Shape: [batch_size, seq_len]. Defaults to None. - news_emb (torch.Tensor, optional): News embedding tensor. Shape: [batch_size, news_dim]. Defaults to None. - - Returns: - Tuple[torch.Tensor, torch.Tensor]: - - s1 logits: Logits for s1 token predictions. Shape: [batch_size, seq_len, s1_vocab_size] - - context: Context representation from the Transformer. Shape: [batch_size, seq_len, d_model] - """ - x = self.embedding([s1_ids, s2_ids]) - if stamp is not None: - time_embedding = self.time_emb(stamp) - x = x + time_embedding - x = self.token_drop(x) - - for layer in self.transformer: - x = layer(x, key_padding_mask=padding_mask) - - x = self.norm(x) - - if news_emb is not None and self.news_proj is not None: - news_bias = self.news_proj(news_emb).unsqueeze(1) # [B, 1, d_model] - x = x + news_bias - - s1_logits = self.head(x) - return s1_logits, x - - def decode_s2(self, context, s1_ids, padding_mask=None): - """ - Decodes the s2 tokens, conditioned on the context and s1 tokens. - - This method decodes s2 tokens based on a pre-computed context representation (typically from `decode_s1`) - and the s1 token IDs. It uses the dependency-aware layer and the conditional s2 head to predict s2 tokens. - - Args: - context (torch.Tensor): Context representation from the transformer (output of decode_s1). - Shape: [batch_size, seq_len, d_model] - s1_ids (torch.torch.Tensor): Input tensor of s1 token IDs. Shape: [batch_size, seq_len] - padding_mask (torch.Tensor, optional): Mask for padding tokens. Shape: [batch_size, seq_len]. Defaults to None. - - Returns: - torch.Tensor: s2 logits. Shape: [batch_size, seq_len, s2_vocab_size] - """ - sibling_embed = self.embedding.emb_s1(s1_ids) - x2 = self.dep_layer(context, sibling_embed, key_padding_mask=padding_mask) - return self.head.cond_forward(x2) - - -def top_k_top_p_filtering( - logits, - top_k: int = 0, - top_p: float = 1.0, - filter_value: float = -float("Inf"), - min_tokens_to_keep: int = 1, -): - """Filter a distribution of logits using top-k and/or nucleus (top-p) filtering - Args: - logits: logits distribution shape (batch size, vocabulary size) - if top_k > 0: keep only top k tokens with highest probability (top-k filtering). - if top_p < 1.0: keep the top tokens with cumulative probability >= top_p (nucleus filtering). - Nucleus filtering is described in Holtzman et al. (http://arxiv.org/abs/1904.09751) - Make sure we keep at least min_tokens_to_keep per batch example in the output - From: https://gist.github.com/thomwolf/1a5a29f6962089e871b94cbd09daf317 - """ - if top_k > 0: - top_k = min(max(top_k, min_tokens_to_keep), logits.size(-1)) # Safety check - # Remove all tokens with a probability less than the last token of the top-k - indices_to_remove = logits < torch.topk(logits, top_k)[0][..., -1, None] - logits[indices_to_remove] = filter_value - return logits - - if top_p < 1.0: - sorted_logits, sorted_indices = torch.sort(logits, descending=True) - cumulative_probs = torch.cumsum(F.softmax(sorted_logits, dim=-1), dim=-1) - - # Remove tokens with cumulative probability above the threshold (token with 0 are kept) - sorted_indices_to_remove = cumulative_probs > top_p - if min_tokens_to_keep > 1: - # Keep at least min_tokens_to_keep (set to min_tokens_to_keep-1 because we add the first one below) - sorted_indices_to_remove[..., :min_tokens_to_keep] = 0 - # Shift the indices to the right to keep also the first token above the threshold - sorted_indices_to_remove[..., 1:] = sorted_indices_to_remove[..., :-1].clone() - sorted_indices_to_remove[..., 0] = 0 - - # scatter sorted tensors to original indexing - indices_to_remove = sorted_indices_to_remove.scatter(1, sorted_indices, sorted_indices_to_remove) - logits[indices_to_remove] = filter_value - return logits - - -def sample_from_logits(logits, temperature=1.0, top_k=None, top_p=None, sample_logits=True): - logits = logits / temperature - if top_k is not None or top_p is not None: - if top_k > 0 or top_p < 1.0: - logits = top_k_top_p_filtering(logits, top_k=top_k, top_p=top_p) - - probs = F.softmax(logits, dim=-1) - - if not sample_logits: - _, x = top_k(probs, k=1, dim=-1) - else: - x = torch.multinomial(probs, num_samples=1) - - return x - - -def auto_regressive_inference(tokenizer, model, x, x_stamp, y_stamp, max_context, pred_len, clip=5, T=1.0, top_k=0, top_p=0.99, sample_count=5, verbose=False, news_emb=None): - with torch.no_grad(): - x = torch.clip(x, -clip, clip) - - device = x.device - x = x.unsqueeze(1).repeat(1, sample_count, 1, 1).reshape(-1, x.size(1), x.size(2)).to(device) - x_stamp = x_stamp.unsqueeze(1).repeat(1, sample_count, 1, 1).reshape(-1, x_stamp.size(1), x_stamp.size(2)).to(device) - y_stamp = y_stamp.unsqueeze(1).repeat(1, sample_count, 1, 1).reshape(-1, y_stamp.size(1), y_stamp.size(2)).to(device) - - x_token = tokenizer.encode(x, half=True) - - initial_seq_len = x.size(1) - batch_size = x_token[0].size(0) - total_seq_len = initial_seq_len + pred_len - full_stamp = torch.cat([x_stamp, y_stamp], dim=1) - - generated_pre = x_token[0].new_empty(batch_size, pred_len) - generated_post = x_token[1].new_empty(batch_size, pred_len) - - pre_buffer = x_token[0].new_zeros(batch_size, max_context) - post_buffer = x_token[1].new_zeros(batch_size, max_context) - buffer_len = min(initial_seq_len, max_context) - if buffer_len > 0: - start_idx = max(0, initial_seq_len - max_context) - pre_buffer[:, :buffer_len] = x_token[0][:, start_idx:start_idx + buffer_len] - post_buffer[:, :buffer_len] = x_token[1][:, start_idx:start_idx + buffer_len] - - if verbose: - ran = trange - else: - ran = range - for i in ran(pred_len): - current_seq_len = initial_seq_len + i - window_len = min(current_seq_len, max_context) - - if current_seq_len <= max_context: - input_tokens = [ - pre_buffer[:, :window_len], - post_buffer[:, :window_len] - ] - else: - input_tokens = [pre_buffer, post_buffer] - - context_end = current_seq_len - context_start = max(0, context_end - max_context) - current_stamp = full_stamp[:, context_start:context_end, :].contiguous() - - s1_logits, context = model.decode_s1(input_tokens[0], input_tokens[1], current_stamp, news_emb=news_emb) - s1_logits = s1_logits[:, -1, :] - sample_pre = sample_from_logits(s1_logits, temperature=T, top_k=top_k, top_p=top_p, sample_logits=True) - - s2_logits = model.decode_s2(context, sample_pre) - s2_logits = s2_logits[:, -1, :] - sample_post = sample_from_logits(s2_logits, temperature=T, top_k=top_k, top_p=top_p, sample_logits=True) - - generated_pre[:, i] = sample_pre.squeeze(-1) - generated_post[:, i] = sample_post.squeeze(-1) - - if current_seq_len < max_context: - pre_buffer[:, current_seq_len] = sample_pre.squeeze(-1) - post_buffer[:, current_seq_len] = sample_post.squeeze(-1) - else: - pre_buffer.copy_(torch.roll(pre_buffer, shifts=-1, dims=1)) - post_buffer.copy_(torch.roll(post_buffer, shifts=-1, dims=1)) - pre_buffer[:, -1] = sample_pre.squeeze(-1) - post_buffer[:, -1] = sample_post.squeeze(-1) - - full_pre = torch.cat([x_token[0], generated_pre], dim=1) - full_post = torch.cat([x_token[1], generated_post], dim=1) - - context_start = max(0, total_seq_len - max_context) - input_tokens = [ - full_pre[:, context_start:total_seq_len].contiguous(), - full_post[:, context_start:total_seq_len].contiguous() - ] - z = tokenizer.decode(input_tokens, half=True) - z = z.reshape(-1, sample_count, z.size(1), z.size(2)) - preds = z.cpu().numpy() - preds = np.mean(preds, axis=1) - - return preds - - -def calc_time_stamps(x_timestamp): - time_df = pd.DataFrame() - time_df['minute'] = x_timestamp.dt.minute - time_df['hour'] = x_timestamp.dt.hour - time_df['weekday'] = x_timestamp.dt.weekday - time_df['day'] = x_timestamp.dt.day - time_df['month'] = x_timestamp.dt.month - return time_df - - -class KronosPredictor: - - def __init__(self, model, tokenizer, device="cuda:0", max_context=512, clip=5): - self.tokenizer = tokenizer - self.model = model - self.max_context = max_context - self.clip = clip - self.price_cols = ['open', 'high', 'low', 'close'] - self.vol_col = 'volume' - self.amt_vol = 'amount' - self.time_cols = ['minute', 'hour', 'weekday', 'day', 'month'] - self.device = device - - self.tokenizer = self.tokenizer.to(self.device) - self.model = self.model.to(self.device) - - def generate(self, x, x_stamp, y_stamp, pred_len, T, top_k, top_p, sample_count, verbose, news_emb=None): - - x_tensor = torch.from_numpy(np.array(x).astype(np.float32)).to(self.device) - x_stamp_tensor = torch.from_numpy(np.array(x_stamp).astype(np.float32)).to(self.device) - y_stamp_tensor = torch.from_numpy(np.array(y_stamp).astype(np.float32)).to(self.device) - - preds = auto_regressive_inference(self.tokenizer, self.model, x_tensor, x_stamp_tensor, y_stamp_tensor, self.max_context, pred_len, - self.clip, T, top_k, top_p, sample_count, verbose, news_emb=news_emb) - preds = preds[:, -pred_len:, :] - return preds - - def predict(self, df, x_timestamp, y_timestamp, pred_len, T=1.0, top_k=0, top_p=0.9, sample_count=1, verbose=True, news_emb=None): - - if not isinstance(df, pd.DataFrame): - raise ValueError("Input must be a pandas DataFrame.") - - if not all(col in df.columns for col in self.price_cols): - raise ValueError(f"Price columns {self.price_cols} not found in DataFrame.") - - df = df.copy() - if self.vol_col not in df.columns: - df[self.vol_col] = 0.0 # Fill missing volume with zeros - df[self.amt_vol] = 0.0 # Fill missing amount with zeros - if self.amt_vol not in df.columns and self.vol_col in df.columns: - df[self.amt_vol] = df[self.vol_col] * df[self.price_cols].mean(axis=1) - - if df[self.price_cols + [self.vol_col, self.amt_vol]].isnull().values.any(): - raise ValueError("Input DataFrame contains NaN values in price or volume columns.") - - x_time_df = calc_time_stamps(x_timestamp) - y_time_df = calc_time_stamps(y_timestamp) - - x = df[self.price_cols + [self.vol_col, self.amt_vol]].values.astype(np.float32) - x_stamp = x_time_df.values.astype(np.float32) - y_stamp = y_time_df.values.astype(np.float32) - - x_mean, x_std = np.mean(x, axis=0), np.std(x, axis=0) - - x = (x - x_mean) / (x_std + 1e-5) - x = np.clip(x, -self.clip, self.clip) - - x = x[np.newaxis, :] - x_stamp = x_stamp[np.newaxis, :] - y_stamp = y_stamp[np.newaxis, :] - - if news_emb is not None: - news_emb_tensor = torch.from_numpy(np.array(news_emb).astype(np.float32)).to(self.device) - # Ensure batch dimension for news_emb if only one sample - if news_emb_tensor.ndim == 1: - news_emb_tensor = news_emb_tensor.unsqueeze(0) - else: - news_emb_tensor = None - - preds = self.generate(x, x_stamp, y_stamp, pred_len, T, top_k, top_p, sample_count, verbose, news_emb=news_emb_tensor) - - preds = preds.squeeze(0) - preds = preds * (x_std + 1e-5) + x_mean - - pred_df = pd.DataFrame(preds, columns=self.price_cols + [self.vol_col, self.amt_vol], index=y_timestamp) - return pred_df - - - def predict_batch(self, df_list, x_timestamp_list, y_timestamp_list, pred_len, T=1.0, top_k=0, top_p=0.9, sample_count=1, verbose=True): - """ - Perform parallel (batch) prediction on multiple time series. All series must have the same historical length and prediction length (pred_len). - - Args: - df_list (List[pd.DataFrame]): List of input DataFrames, each containing price columns and optional volume/amount columns. - x_timestamp_list (List[pd.DatetimeIndex or Series]): List of timestamps corresponding to historical data, length should match the number of rows in each DataFrame. - y_timestamp_list (List[pd.DatetimeIndex or Series]): List of future prediction timestamps, length should equal pred_len. - pred_len (int): Number of prediction steps. - T (float): Sampling temperature. - top_k (int): Top-k filtering threshold. - top_p (float): Top-p (nucleus sampling) threshold. - sample_count (int): Number of parallel samples per series, automatically averaged internally. - verbose (bool): Whether to display autoregressive progress. - - Returns: - List[pd.DataFrame]: List of prediction results in the same order as input, each DataFrame contains - `open, high, low, close, volume, amount` columns, indexed by corresponding `y_timestamp`. - """ - # Basic validation - if not isinstance(df_list, (list, tuple)) or not isinstance(x_timestamp_list, (list, tuple)) or not isinstance(y_timestamp_list, (list, tuple)): - raise ValueError("df_list, x_timestamp_list, y_timestamp_list must be list or tuple types.") - if not (len(df_list) == len(x_timestamp_list) == len(y_timestamp_list)): - raise ValueError("df_list, x_timestamp_list, y_timestamp_list must have consistent lengths.") - - num_series = len(df_list) - - x_list = [] - x_stamp_list = [] - y_stamp_list = [] - means = [] - stds = [] - seq_lens = [] - y_lens = [] - - for i in range(num_series): - df = df_list[i] - if not isinstance(df, pd.DataFrame): - raise ValueError(f"Input at index {i} is not a pandas DataFrame.") - if not all(col in df.columns for col in self.price_cols): - raise ValueError(f"DataFrame at index {i} is missing price columns {self.price_cols}.") - - df = df.copy() - if self.vol_col not in df.columns: - df[self.vol_col] = 0.0 - df[self.amt_vol] = 0.0 - if self.amt_vol not in df.columns and self.vol_col in df.columns: - df[self.amt_vol] = df[self.vol_col] * df[self.price_cols].mean(axis=1) - - if df[self.price_cols + [self.vol_col, self.amt_vol]].isnull().values.any(): - raise ValueError(f"DataFrame at index {i} contains NaN values in price or volume columns.") - - x_timestamp = x_timestamp_list[i] - y_timestamp = y_timestamp_list[i] - - x_time_df = calc_time_stamps(x_timestamp) - y_time_df = calc_time_stamps(y_timestamp) - - x = df[self.price_cols + [self.vol_col, self.amt_vol]].values.astype(np.float32) - x_stamp = x_time_df.values.astype(np.float32) - y_stamp = y_time_df.values.astype(np.float32) - - if x.shape[0] != x_stamp.shape[0]: - raise ValueError(f"Inconsistent lengths at index {i}: x has {x.shape[0]} vs x_stamp has {x_stamp.shape[0]}.") - if y_stamp.shape[0] != pred_len: - raise ValueError(f"y_timestamp length at index {i} should equal pred_len={pred_len}, got {y_stamp.shape[0]}.") - - x_mean, x_std = np.mean(x, axis=0), np.std(x, axis=0) - x_norm = (x - x_mean) / (x_std + 1e-5) - x_norm = np.clip(x_norm, -self.clip, self.clip) - - x_list.append(x_norm) - x_stamp_list.append(x_stamp) - y_stamp_list.append(y_stamp) - means.append(x_mean) - stds.append(x_std) - - seq_lens.append(x_norm.shape[0]) - y_lens.append(y_stamp.shape[0]) - - # Require all series to have consistent historical and prediction lengths for batch processing - if len(set(seq_lens)) != 1: - raise ValueError(f"Parallel prediction requires all series to have consistent historical lengths, got: {seq_lens}") - if len(set(y_lens)) != 1: - raise ValueError(f"Parallel prediction requires all series to have consistent prediction lengths, got: {y_lens}") - - x_batch = np.stack(x_list, axis=0).astype(np.float32) # (B, seq_len, feat) - x_stamp_batch = np.stack(x_stamp_list, axis=0).astype(np.float32) # (B, seq_len, time_feat) - y_stamp_batch = np.stack(y_stamp_list, axis=0).astype(np.float32) # (B, pred_len, time_feat) - - preds = self.generate(x_batch, x_stamp_batch, y_stamp_batch, pred_len, T, top_k, top_p, sample_count, verbose) - # preds: (B, pred_len, feat) - - pred_dfs = [] - for i in range(num_series): - preds_i = preds[i] * (stds[i] + 1e-5) + means[i] - pred_df = pd.DataFrame(preds_i, columns=self.price_cols + [self.vol_col, self.amt_vol], index=y_timestamp_list[i]) - pred_dfs.append(pred_df) - - return pred_dfs diff --git a/skills/alphaear-predictor/scripts/predictor/model/module.py b/skills/alphaear-predictor/scripts/predictor/model/module.py deleted file mode 100644 index 20b29b5..0000000 --- a/skills/alphaear-predictor/scripts/predictor/model/module.py +++ /dev/null @@ -1,562 +0,0 @@ -import math - -from einops import rearrange, reduce -import torch -import torch.nn as nn -from torch.autograd import Function -import torch.nn.functional as F - - -class DifferentiableEntropyFunction(Function): - @staticmethod - def forward(ctx, zq, basis, K, eps): - zb = (zq + 1) / 2 - zi = ((zb * basis).sum(-1)).to(torch.int64) - cnt = torch.scatter_reduce(torch.zeros(2 ** K, device=zq.device, dtype=zq.dtype), - 0, - zi.flatten(), - torch.ones_like(zi.flatten()).to(zq.dtype), - 'sum') - prob = (cnt + eps) / (cnt + eps).sum() - H = -(prob * torch.log(prob)).sum() - ctx.save_for_backward(zq, zi, prob) - ctx.K = K - return H - - @staticmethod - def backward(ctx, grad_output): - zq, zi, prob = ctx.saved_tensors - grad_array = -grad_output * (torch.log(prob) + 1) / zi.numel() / ctx.K - reord_grad = grad_array[zi.flatten()].reshape(zi.shape) - grad_input = reord_grad.unsqueeze(-1) * zq - return grad_input, None, None, None, None - - -def codebook_entropy(zq, basis, K, eps=1e-4): - return DifferentiableEntropyFunction.apply(zq, basis, K, eps) - - -class BinarySphericalQuantizer(nn.Module): - def __init__(self, embed_dim, beta, gamma0, gamma, zeta, - input_format='bchw', - soft_entropy=True, group_size=9, - persample_entropy_compute='analytical', - cb_entropy_compute='group', - l2_norm=True, - inv_temperature=1): - """ - Paper link: https://arxiv.org/pdf/2406.07548.pdf - Here we use the official implementation of the BinarySphericalQuantizer. - """ - super().__init__() - self.embed_dim = embed_dim - self.beta = beta # loss weight for commit loss - self.gamma0 = gamma0 # loss weight for entropy penalty - self.gamma = gamma # loss weight for entropy penalty - self.zeta = zeta # loss weight for entire entropy penalty - self.input_format = input_format - assert self.embed_dim % group_size == 0, "embed_dim must be divisible by group_size" - self.num_groups = self.embed_dim // group_size - self.group_size = group_size - assert persample_entropy_compute in ['group', 'analytical'], "persample_entropy_compute must be either 'group' or 'analytical'" - assert cb_entropy_compute in ['group', 'nce'], "cb_entropy_compute must be either 'group' or 'nce'" - self.persample_entropy_compute = persample_entropy_compute - self.cb_entropy_compute = cb_entropy_compute - self.l2_norm = l2_norm - self.inv_temperature = inv_temperature - - self.register_buffer('basis', 2 ** torch.arange(embed_dim - 1, -1, -1)) - self.register_buffer('group_basis', 2 ** torch.arange(group_size - 1, -1, -1)) - - self.num_dimensions = 2 ** embed_dim - self.bits_per_index = embed_dim - - # we only need to keep the codebook portion up to the group size - # because we approximate the H loss with this subcode - group_codes = torch.arange(2 ** self.group_size) - group_codebook = self.indexes_to_codes(group_codes).float()[:, -group_size:] - self.register_buffer('group_codebook', group_codebook, persistent=False) - - self.soft_entropy = soft_entropy # soft_entropy: Sec 3.2 of https://arxiv.org/pdf/1911.05894.pdf - - def quantize(self, z): - assert z.shape[-1] == self.embed_dim, f"Expected {self.embed_dim} dimensions, got {z.shape[-1]}" - - zhat = torch.where(z > 0, - torch.tensor(1, dtype=z.dtype, device=z.device), - torch.tensor(-1, dtype=z.dtype, device=z.device)) - return z + (zhat - z).detach() - - def forward(self, z, collect_metrics=True): - # if self.input_format == 'bchw': - # z = rearrange(z, 'b c h w -> b h w c') - zq = self.quantize(z) - - q_scale = 1. / (self.embed_dim ** 0.5) if self.l2_norm else 1. - - zq = zq * q_scale - - if not collect_metrics: - return zq, zq.new_zeros(()), {} - - indices = self.codes_to_indexes(zq.detach()) - group_indices = self.codes_to_group_indexes(zq.detach()) - if not self.training: - used_codes = torch.unique(indices, return_counts=False) - else: - used_codes = None - - if self.soft_entropy: - persample_entropy, cb_entropy, avg_prob = self.soft_entropy_loss(z) - entropy_penalty = self.gamma0 * persample_entropy - self.gamma * cb_entropy - else: - zb_by_sample = ((zq + 1) / 2).reshape(z.shape[0], -1, z.shape[-1]).to(torch.float32) - persample_entropy = self.get_hard_per_sample_entropy(zb_by_sample) - cb_entropy = codebook_entropy(zq, self.basis, self.embed_dim) - entropy_penalty = self.gamma0 * persample_entropy - self.gamma * cb_entropy - - # commit loss - commit_loss = self.beta * torch.mean(((zq.detach() - z) ** 2).sum(dim=-1)) - - # if self.input_format == 'bchw': - # zq = rearrange(zq, 'b h w c -> b c h w') - - return ( - zq, - commit_loss + self.zeta * entropy_penalty / self.inv_temperature, - {"H": cb_entropy, "used_codes": used_codes, "indices": indices, "group_indices": group_indices, - "avg_prob": avg_prob} - ) - - def soft_entropy_loss(self, z): - # if we divide the code in subgroups of size group_size, the codebook will be of size 2 ** group_size - # the sub-code is the last group_size bits of the full code - group_code_book = self.group_codebook / (self.embed_dim ** 0.5 if self.l2_norm else 1) - divided_z = rearrange(z, '... (g c) -> ... g c', c=self.group_size) - - # we calculate the distance between the divided_z and the codebook for each subgroup - distance = - 2 * torch.einsum('... g c, d c ->... g d', divided_z, group_code_book) - prob = (-distance * self.inv_temperature).softmax(dim=-1) - if self.persample_entropy_compute == 'analytical': - if self.l2_norm: - p = torch.sigmoid(-4 * z / (self.embed_dim ** 0.5) * self.inv_temperature) - else: - p = torch.sigmoid(-4 * z * self.inv_temperature) - prob = torch.stack([p, 1 - p], dim=-1) - per_sample_entropy = self.get_entropy(prob, dim=-1, normalize=False).sum(dim=-1).mean() - else: - per_sample_entropy = self.get_entropy(prob, dim=-1, normalize=False).sum(dim=-1).mean() - - # macro average of the probability of each subgroup - avg_prob = reduce(prob, '... g d ->g d', 'mean') - codebook_entropy = self.get_entropy(avg_prob, dim=-1, normalize=False) - - # the approximation of the entropy is the sum of the entropy of each subgroup - return per_sample_entropy, codebook_entropy.sum(), avg_prob - - def get_hard_per_sample_entropy(self, zb_by_sample): - probs_per_dim = zb_by_sample.sum(1) / zb_by_sample.shape[1] - persample_entropy = - probs_per_dim * torch.log(probs_per_dim + 1e-8) - (1 - probs_per_dim) * torch.log(1 - probs_per_dim + 1e-8) - persample_entropy = persample_entropy.sum(-1) - return persample_entropy.mean() - - def codes_to_indexes(self, zhat): - """Converts a `code` to an index in the codebook. - Args: - zhat: A tensor of shape (B, ..., C) containing the codes. must be in {-1, 1} - """ - assert zhat.shape[-1] == self.embed_dim, f"Expected {self.embed_dim} dimensions, got {zhat.shape[-1]}" - return ((zhat + 1) / 2 * self.basis).sum(axis=-1).to(torch.int64) - - def codes_to_group_indexes(self, zhat): - """Converts a `code` to a list of indexes (in groups) in the codebook. - Args: - zhat: A tensor of shape (B, ..., C) containing the codes. must be in {-1, 1} - """ - zhat_in_group = rearrange(zhat, 'b ... (g c) -> b ... g c', c=self.group_size) - return ((zhat_in_group + 1) / 2 * self.group_basis).sum(axis=-1).to(torch.int64) - - def indexes_to_codes(self, indices): - """Inverse of `indexes_to_codes`.""" - indices = indices.unsqueeze(-1) - codes_non_centered = torch.remainder( - torch.floor_divide(indices, self.basis), 2 - ) - return codes_non_centered * 2 - 1 - - def group_indexes_to_codes(self, group_indices): - """Inverse of `group_indexes_to_codes`.""" - group_indices = group_indices.unsqueeze(-1) - codes_non_centered = torch.remainder( - torch.floor_divide(group_indices, self.group_basis), 2 - ) - codes_non_centered = rearrange(codes_non_centered, 'b ... g c -> b ... (g c)') - return codes_non_centered * 2 - 1 - - def get_entropy(self, count, dim=-1, eps=1e-4, normalize=True): - if normalize: - probs = (count + eps) / (count + eps).sum(dim=dim, keepdim=True) - else: - probs = count - H = -(probs * torch.log(probs + 1e-8)).sum(dim=dim) - return H - - def get_group_codebook_entry(self, group_indices): - z_q = self.group_indexes_to_codes(group_indices) - q_scale = 1. / (self.embed_dim ** 0.5) if self.l2_norm else 1. - z_q = z_q * q_scale - if self.input_format == 'bchw': - h, w = int(z_q.shape[1] ** 0.5) - assert h * w == z_q.shape[1], 'Invalid sequence length' - z_q = rearrange(z_q, 'b (h w) c -> b c h w', h=h) - return z_q - - def get_codebook_entry(self, indices): - z_q = self.indexes_to_codes(indices) - q_scale = 1. / (self.embed_dim ** 0.5) if self.l2_norm else 1. - z_q = z_q * q_scale - if self.input_format == 'bchw': - h, w = int(z_q.shape[1] ** 0.5) - assert h * w == z_q.shape[1], 'Invalid sequence length' - z_q = rearrange(z_q, 'b (h w) c -> b c h w', h=h) - return z_q - - -class BSQuantizer(nn.Module): - - def __init__(self, s1_bits, s2_bits, beta, gamma0, gamma, zeta, group_size): - super().__init__() - self.codebook_dim = s1_bits + s2_bits - self.s1_bits = s1_bits - self.s2_bits = s2_bits - self.bsq = BinarySphericalQuantizer(self.codebook_dim, beta, gamma0, gamma, zeta, group_size=group_size) - - def bits_to_indices(self, bits): - bits = (bits >= 0).to(torch.long) - indices = 2 ** torch.arange( - 0, - bits.shape[-1], - 1, - dtype=torch.long, - device=bits.device, - ) - return (bits * indices).sum(-1) - - def forward(self, z, half=False, collect_metrics=True): - z = F.normalize(z, dim=-1) - quantized, bsq_loss, metrics = self.bsq(z, collect_metrics=collect_metrics) - if half: - q_pre = quantized[:, :, :self.s1_bits] - q_post = quantized[:, :, self.s1_bits:] - z_indices = [self.bits_to_indices(q_pre), self.bits_to_indices(q_post)] - else: - z_indices = self.bits_to_indices(quantized) - return bsq_loss, quantized, z_indices - - -class RMSNorm(torch.nn.Module): - def __init__(self, dim: int, eps: float = 1e-5): - super().__init__() - self.eps = eps - self.weight = nn.Parameter(torch.ones(dim)) - - def _norm(self, x): - return x * torch.rsqrt(torch.mean(x * x, dim=-1, keepdim=True) + self.eps) - - def forward(self, x): - output = self._norm(x.float()).type_as(x) - return output * self.weight - - -class FeedForward(nn.Module): - def __init__(self, d_model, ff_dim, ffn_dropout_p=0.0): - super().__init__() - - self.w1 = nn.Linear(d_model, ff_dim, bias=False) - self.w3 = nn.Linear(d_model, ff_dim, bias=False) - self.w2 = nn.Linear(ff_dim, d_model, bias=False) - self.ffn_dropout = nn.Dropout(ffn_dropout_p) - - def forward(self, x): - return self.ffn_dropout(self.w2(F.silu(self.w1(x)) * self.w3(x))) - - -class RotaryPositionalEmbedding(nn.Module): - def __init__(self, dim): - super().__init__() - inv_freq = 1.0 / (10000 ** (torch.arange(0, dim, 2).float() / dim)) - self.register_buffer("inv_freq", inv_freq) - self.seq_len_cached = None - self.cos_cached = None - self.sin_cached = None - - def _update_cos_sin_cache(self, x, seq_len): - if seq_len != self.seq_len_cached: - self.seq_len_cached = seq_len - t = torch.arange(seq_len, device=x.device).type_as(self.inv_freq) - freqs = torch.einsum('i,j->ij', t, self.inv_freq) - emb = torch.cat((freqs, freqs), dim=-1).to(x.device) - self.cos_cached = emb.cos()[None, None, :, :] - self.sin_cached = emb.sin()[None, None, :, :] - return self.cos_cached, self.sin_cached - - def forward(self, q, k): - cos, sin = self._update_cos_sin_cache(q, q.shape[-2]) - return ( - (q * cos) + (self._rotate_half(q) * sin), - (k * cos) + (self._rotate_half(k) * sin), - ) - - def _rotate_half(self, x): - x1, x2 = x.chunk(2, dim=-1) - return torch.cat((-x2, x1), dim=-1) - - -class MultiHeadAttentionWithRoPE(nn.Module): - def __init__(self, d_model, n_heads, attn_dropout_p=0.0, resid_dropout_p=0.0): - super().__init__() - self.d_model = d_model - self.n_heads = n_heads - self.head_dim = d_model // n_heads - - self.q_proj = nn.Linear(d_model, d_model) - self.k_proj = nn.Linear(d_model, d_model) - self.v_proj = nn.Linear(d_model, d_model) - self.out_proj = nn.Linear(d_model, d_model) - self.rotary = RotaryPositionalEmbedding(self.head_dim) - self.attn_dropout_p = attn_dropout_p - self.resid_dropout = nn.Dropout(resid_dropout_p) - - def forward(self, x, key_padding_mask=None): - batch_size, seq_len, _ = x.shape - - q = self.q_proj(x).view(batch_size, seq_len, self.n_heads, self.head_dim).transpose(1, 2) - k = self.k_proj(x).view(batch_size, seq_len, self.n_heads, self.head_dim).transpose(1, 2) - v = self.v_proj(x).view(batch_size, seq_len, self.n_heads, self.head_dim).transpose(1, 2) - - q, k = self.rotary(q, k) - - if key_padding_mask is not None: - attn_mask = key_padding_mask.unsqueeze(1).unsqueeze(2) # [batch, 1, 1, seq_len] - attn_mask = attn_mask.expand(-1, self.n_heads, seq_len, -1) # [batch, n_heads, q_len, k_len] - else: - attn_mask = None - - attn_output = F.scaled_dot_product_attention( - q, k, v, - attn_mask=attn_mask, - dropout_p=self.attn_dropout_p if self.training else 0.0, - is_causal=True - ) - - attn_output = attn_output.transpose(1, 2).contiguous().view(batch_size, seq_len, self.d_model) - return self.resid_dropout(self.out_proj(attn_output)) - - -class MultiHeadCrossAttentionWithRoPE(nn.Module): - def __init__(self, d_model, n_heads, attn_dropout_p=0.0, resid_dropout=0.0): - super().__init__() - self.d_model = d_model - self.n_heads = n_heads - self.head_dim = d_model // n_heads - - self.q_proj = nn.Linear(d_model, d_model) - self.k_proj = nn.Linear(d_model, d_model) - self.v_proj = nn.Linear(d_model, d_model) - self.out_proj = nn.Linear(d_model, d_model) - self.rotary = RotaryPositionalEmbedding(self.head_dim) - self.attn_dropout_p = attn_dropout_p - self.resid_dropout = nn.Dropout(resid_dropout) - - def forward(self, query, key, value, key_padding_mask=None): - batch_size, q_len, _ = query.shape - _, seq_len, _ = key.shape - - q = self.q_proj(query).view(batch_size, q_len, self.n_heads, self.head_dim).transpose(1, 2) - k = self.k_proj(key).view(batch_size, seq_len, self.n_heads, self.head_dim).transpose(1, 2) - v = self.v_proj(value).view(batch_size, seq_len, self.n_heads, self.head_dim).transpose(1, 2) - - q, k = self.rotary(q, k) - - if key_padding_mask is not None: - attn_mask = key_padding_mask.unsqueeze(1).unsqueeze(2) - attn_mask = attn_mask.expand(-1, self.n_heads, q_len, -1) - else: - attn_mask = None - - is_causal_flag = self.training - - attn_output = F.scaled_dot_product_attention( - q, k, v, - attn_mask=attn_mask, - dropout_p=self.attn_dropout_p if self.training else 0.0, - is_causal=is_causal_flag - ) - - attn_output = attn_output.transpose(1, 2).contiguous().view(batch_size, q_len, self.d_model) - return self.resid_dropout(self.out_proj(attn_output)) - - -class HierarchicalEmbedding(nn.Module): - def __init__(self, s1_bits, s2_bits, d_model=256): - super().__init__() - self.s1_bits = s1_bits - self.s2_bits = s2_bits - - vocab_s1 = 2 ** s1_bits - vocab_s2 = 2 ** s2_bits - - self.emb_s1 = nn.Embedding(vocab_s1, d_model) - self.emb_s2 = nn.Embedding(vocab_s2, d_model) - self.d_model = d_model - self.fusion_proj = nn.Linear(d_model * 2, d_model) - - nn.init.normal_(self.emb_s1.weight, mean=0, std=d_model ** -0.5) - nn.init.normal_(self.emb_s2.weight, mean=0, std=d_model ** -0.5) - - def split_token(self, token_ids: torch.Tensor, s2_bits: int): - """Inputs: - token_ids (torch.Tensor): Composite token IDs of shape [batch_size, seq_len] or [N], each in range [0, 2^(s1_bits + s2_bits) - 1]. - s2_bits (int): Number of low bits used for the fine token (s2). - """ - assert isinstance(s2_bits, int) and s2_bits > 0, "s2_bits must be a positive integer" - - t = token_ids.long() - mask = (1 << s2_bits) - 1 - s2_ids = t & mask # extract low bits - s1_ids = t >> s2_bits # extract high bits - return s1_ids, s2_ids - - def forward(self, token_ids): - """Inputs: - token_ids: - - tuple or list: (s1_ids, s2_ids), each of shape [batch_size, seq_len], or - - torch.Tensor: composite token IDs of shape [batch_size, seq_len], which will be split into (s1_ids, s2_ids) internally. - Output: [batch_size, seq_len, d_model] - """ - if isinstance(token_ids, tuple) or isinstance(token_ids, list): - s1_ids, s2_ids = token_ids - else: - s1_ids, s2_ids = self.split_token(token_ids, self.s2_bits) - s1_emb = self.emb_s1(s1_ids) * math.sqrt(self.d_model) - s2_emb = self.emb_s2(s2_ids) * math.sqrt(self.d_model) - return self.fusion_proj(torch.cat([s1_emb, s2_emb], dim=-1)) - - -class DependencyAwareLayer(nn.Module): - def __init__(self, d_model, n_heads=4, attn_dropout_p=0.0, resid_dropout=0.0): - super().__init__() - self.cross_attn = MultiHeadCrossAttentionWithRoPE(d_model, n_heads, attn_dropout_p, resid_dropout) - self.norm = RMSNorm(d_model) - - def forward(self, hidden_states, sibling_embed, key_padding_mask=None): - """hidden_states: [batch, seq_len, d_model] - sibling_embed: Embedding from another subtoken - """ - attn_out = self.cross_attn( - query=sibling_embed, - key=hidden_states, - value=hidden_states, - key_padding_mask=key_padding_mask - ) - return self.norm(hidden_states + attn_out) - - -class TransformerBlock(nn.Module): - def __init__(self, d_model, n_heads, ff_dim=1024, ffn_dropout_p=0.0, attn_dropout_p=0.0, resid_dropout_p=0.0): - super().__init__() - self.norm1 = RMSNorm(d_model) - self.self_attn = MultiHeadAttentionWithRoPE(d_model, n_heads, attn_dropout_p, resid_dropout_p) - self.norm2 = RMSNorm(d_model) - self.ffn = FeedForward(d_model, ff_dim, ffn_dropout_p) - - def forward(self, x, key_padding_mask=None): - residual = x - x = self.norm1(x) - attn_out = self.self_attn(x, key_padding_mask=key_padding_mask) - x = residual + attn_out - - residual = x - x = self.norm2(x) - ffn_out = self.ffn(x) - x = residual + ffn_out - return x - - -class DualHead(nn.Module): - def __init__(self, s1_bits, s2_bits, d_model): - super().__init__() - self.vocab_s1 = 2 ** s1_bits - self.vocab_s2 = 2 ** s2_bits - self.proj_s1 = nn.Linear(d_model, self.vocab_s1) - self.proj_s2 = nn.Linear(d_model, self.vocab_s2) - - def compute_loss(self, s1_logits, s2_logits, s1_targets, s2_targets, padding_mask=None): - if padding_mask is not None: - valid_mask = (padding_mask == 0) - s1_logits = s1_logits[valid_mask] - s2_logits = s2_logits[valid_mask] - s1_targets = s1_targets[valid_mask] - s2_targets = s2_targets[valid_mask] - ce_s1 = F.cross_entropy(s1_logits, s1_targets) - ce_s2 = F.cross_entropy(s2_logits, s2_targets) - else: - ce_s1 = F.cross_entropy(s1_logits.reshape(-1, self.vocab_s1), s1_targets.reshape(-1)) - ce_s2 = F.cross_entropy(s2_logits.reshape(-1, self.vocab_s2), s2_targets.reshape(-1)) - ce_loss = (ce_s1 + ce_s2) / 2 - return ce_loss, ce_s1, ce_s2 - - def forward(self, x): - return self.proj_s1(x) - - def cond_forward(self, x2): - return self.proj_s2(x2) - - -class FixedEmbedding(nn.Module): - def __init__(self, c_in, d_model): - super(FixedEmbedding, self).__init__() - - w = torch.zeros(c_in, d_model).float() - w.require_grad = False - - position = torch.arange(0, c_in).float().unsqueeze(1) - div_term = (torch.arange(0, d_model, 2).float() * -(math.log(10000.0) / d_model)).exp() - - w[:, 0::2] = torch.sin(position * div_term) - w[:, 1::2] = torch.cos(position * div_term) - - self.emb = nn.Embedding(c_in, d_model) - self.emb.weight = nn.Parameter(w, requires_grad=False) - - def forward(self, x): - return self.emb(x).detach() - - -class TemporalEmbedding(nn.Module): - def __init__(self, d_model, learn_pe): - super(TemporalEmbedding, self).__init__() - - minute_size = 60 - hour_size = 24 - weekday_size = 7 - day_size = 32 - month_size = 13 - - Embed = FixedEmbedding if not learn_pe else nn.Embedding - self.minute_embed = Embed(minute_size, d_model) - self.hour_embed = Embed(hour_size, d_model) - self.weekday_embed = Embed(weekday_size, d_model) - self.day_embed = Embed(day_size, d_model) - self.month_embed = Embed(month_size, d_model) - - def forward(self, x): - x = x.long() - - minute_x = self.minute_embed(x[:, :, 0]) - hour_x = self.hour_embed(x[:, :, 1]) - weekday_x = self.weekday_embed(x[:, :, 2]) - day_x = self.day_embed(x[:, :, 3]) - month_x = self.month_embed(x[:, :, 4]) - - return hour_x + weekday_x + day_x + month_x + minute_x \ No newline at end of file diff --git a/skills/alphaear-predictor/scripts/prompts/fin_agent.py b/skills/alphaear-predictor/scripts/prompts/fin_agent.py deleted file mode 100644 index 83386af..0000000 --- a/skills/alphaear-predictor/scripts/prompts/fin_agent.py +++ /dev/null @@ -1,127 +0,0 @@ -from datetime import datetime -from .isq_prompt_generator import generate_isq_prompt_section - -def get_fin_researcher_instructions() -> str: - """生成金融研究员 (Researcher) 的系统指令""" - current_time = datetime.now().strftime('%Y-%m-%d %H:%M:%S') - return f"""你是一名资深金融研究员,当前时间是 {current_time}。 -你的任务是针对给定的“原始信号”进行详尽的背景调查,为后续的深度分析提供素材。 - -### 1. 核心职责 -1. **标的识别**: 识别信号中涉及的具体上市公司。必须调用 `search_ticker` 确认代码,并调用 `get_stock_price` 获取最新价格和近 30 天走势。 -2. **事实核查**: 使用 `web_search` 或 `fetch_news_content` 验证信号的真实性,并寻找更多细节(如公告原文、行业研报摘要)。 -3. **产业链梳理**: 补充该信号涉及的上下游环节及竞争格局。 - -### 2. 工具使用规范 (CRITICAL) -- **每个提到的公司都需要调用工具**: 不能依赖记忆,必须实时查询。 -- **完整呈现工具结果**: 包括具体的股价数字、代码、技术面数据等,不要缩略。 -- **股价数据必需**: 当前价格、近期最高最低、技术面支撑阻力等数据是后续预测的基础。 -- **信息交叉验证**: 多个来源验证关键事实。 - -### 3. 输出要求 -你必须输出结构化的研究报告,涵盖标的基本面、股价走势、行业背景及最新进展。 -""" - -def get_fin_analyst_instructions(template_id: str = "default_isq_v1") -> str: - """生成金融分析师 (Analyst) 的系统指令 - - Args: - template_id: 使用的 ISQ 模板 ID - """ - current_time = datetime.now().strftime('%Y-%m-%d %H:%M:%S') - isq_block = generate_isq_prompt_section(template_id=template_id) - - return f"""你是一位深耕二级市场的资深金融分析师 (FinAgent),当前时间是 {current_time}。 -你的核心任务是执行“信号解析”,将研究员搜集的素材转化为具有可操作性的投资情报(ISQ 框架)。 - -{isq_block} - -### 2. 分析约束 -- **严格基于具体数据**: 必须使用研究员提供的股价、技术面、新闻等具体数据进行分析。 -- **数据驱动的预测**: impact_tickers 中的权重应基于事件影响程度,不能随意赋值。 -- **逻辑严密**: 传导链条必须符合金融常识,能够自圆其说。 -- **技术面参考**: 如果研究员提供了股价走势,请分析当前位置相对于支撑/阻力位的关系。 - -### 3. 关键要求 -- **title**: 必须生成一个简练、准确概括信号核心内容的标题(不超过 15 字)。 -- **impact_tickers**: 必须填充具体的公司代码(6位数字)和名称,权重应该有区分。 -- **transmission_chain**: 必须是对象列表,每个对象包含: - - `node_name`: 节点名称(如“上游原材料”、“中游制造”) - - `impact_type`: 影响类型(“利好”、“利空”、“中性”) - - `logic`: 具体的传导逻辑描述 -- **summary**: 基于分析结果总结核心观点,包含具体数字(如股价目标、预期涨跌幅等)。 -- **reasoning**: 必须详细阐述推演逻辑,解释为什么得出上述结论(<200字)。 - -### 4. 输出格式 (严格 JSON 块) -你必须输出一个符合 InvestmentSignal 结构的 JSON 块,包含所有必需字段。 -""" - -def get_fin_agent_instructions() -> str: - # 保持兼容性,但内部调用 analyst 指令 - return get_fin_analyst_instructions() - -def get_fin_research_task(signal_text: str) -> str: - """生成研究员的任务描述""" - return f"请针对以下信号进行背景调查,搜集相关标的的股价、最新进展和行业背景:\n\n{signal_text}" - -def format_research_context(research_data: dict) -> str: - """将研究员搜集的结构化数据格式化为分析师可读的文本""" - if not research_data: - return "(未能搜集到额外背景信息)" - - return f""" -### 研究背景 -- **相关标的**: {research_data.get('tickers_found', [])} -- **行业背景**: {research_data.get('industry_background', '未知')} -- **最新进展**: {', '.join(research_data.get('latest_developments', []))} -- **关键风险**: {', '.join(research_data.get('key_risks', []))} -- **综合摘要**: {research_data.get('search_results_summary', '无')} -""" - -def get_fin_analysis_task(signal_text: str, research_context_str: str) -> str: - """生成分析师的任务描述""" - return f"""请基于以下信息进行深度 ISQ 分析。关键是:必须使用研究员搜集的具体数据(股价、技术面、新闻、代码等)进行分析。 - -=== 原始信号 === -{signal_text} - -=== 研究员搜集的背景信息 (CRITICAL DATA) === -{research_context_str} - -=== 分析要求 === -1. 必须生成 title:简练概括信号核心(<15字) -2. 基于研究员提供的具体股价数据,分析当前定价状态(已定价/未定价/部分定价) -3. impact_tickers 中填充具体的公司代码和权重,权重基于事件影响程度 -4. transmission_chain 必须是包含 node_name, impact_type, logic 的对象列表 -5. summary 中包含具体数字(预期目标价、涨跌幅范围等) -6. reasoning 必须详细解释推演逻辑,不要空泛,要言之有物 - -请严格按 InvestmentSignal JSON 格式输出。""" - -def get_tracking_analysis_task(old_signal: dict, new_research_str: str) -> str: - """生成信号追踪更新的任务描述""" - import json - old_sig_str = json.dumps(old_signal, ensure_ascii=False, indent=2) - return f"""你正在执行“信号逻辑演变追踪”任务。请基于最新的市场信息,重新评估之前的投资信号。 - -=== 基准信号 (上次分析) === -{old_sig_str} - -=== 最新市场追踪 (NEWS & PRICE) === -{new_research_str} - -=== 追踪分析要求 === -1. **逻辑演变检测**: - - 对比新旧信息,判断原逻辑 (`transmission_chain` 和 `reasoning`) 是否依然成立? - - 如果逻辑发生变化(如利好落空、逻辑证伪、新利好出现),请在新的 `reasoning` 中明确指出“逻辑演变:...” - - 如果逻辑未变且得到验证,请标记“逻辑维持:...” - -2. **参数修正**: - - 根据最新股价和新闻,更新 `sentiment_score` (情绪)、`confidence` (置信度) 和 `expectation_gap` (预期差)。 - - 例如:如果股价已经大涨反映了利好,`expectation_gap` 应该显著降低。 - -3. **输出更新后的信号**: - - 保留原 `signal_id` 和 `title`(除非有重大变化需要改名)。 - - 输出完整的 InvestmentSignal JSON。 - -请重点关注:为什么变了?还是为什么没变?理由要充分。""" diff --git a/skills/alphaear-predictor/scripts/prompts/forecast_analyst.py b/skills/alphaear-predictor/scripts/prompts/forecast_analyst.py deleted file mode 100644 index d6c7202..0000000 --- a/skills/alphaear-predictor/scripts/prompts/forecast_analyst.py +++ /dev/null @@ -1,49 +0,0 @@ -from typing import List, Dict, Any -from ..schema.models import KLinePoint - -def get_forecast_adjustment_instructions(ticker: str, news_context: str, model_forecast: List[KLinePoint]): - """ - 生成 LLM 预测调整指令 - """ - forecast_str = "\n".join([f"- {p.date}: O:{p.open}, C:{p.close}" for p in model_forecast]) - - return f"""你是一位资深的量化策略分析师。 -你的任务是:根据给定的【Kronos 模型预测结果】和【最新的基本面/新闻背景】,对模型预测进行“主观/逻辑调整”。 - -股票代码: {ticker} - -【Kronos 模型原始预测 (OHLC)】: -{forecast_str} - -【最新情报背景】: -{news_context} - -调整原则: -1. 原始预测是基于历史的技术面推演。 -2. 情报背景中可能包含【Kronos模型定量修正预测】,这是基于历史新闻训练的专用模型计算出的量化结果。 -3. 如果存在“定量修正预测”,请**高度参考**该数值作为基础,除非你有非常确凿的逻辑认为该量化模型失效(例如遇到模型未见过的极端黑天鹅)。 -4. 你的核心任务是:结合定性分析(新闻及其逻辑)来验证或微调这些数字,并给出合理的解释(Rationale)。 -5. 如果没有“定量修正预测”,则你需要根据新闻信号手动大幅调整趋势。 - -输出要求 (严格 JSON 格式): -```json -{{ - "adjusted_forecast": [ - {{ - "date": "YYYY-MM-DD", - "open": float, - "high": float, - "low": float, - "close": float, - "volume": float - }}, - ... - ], - "rationale": "详细说明调整的逻辑依据,例如:考虑到[事件A],预期短线将突破压力位..." -}} -``` -注意:必须输出与原始预测相同数量的数据点,且日期一一对应。 -""" - -def get_forecast_task(): - return "请根据以上背景和模型预测,给出调整后的 K 线数据并说明理由。" diff --git a/skills/alphaear-predictor/scripts/prompts/intent_agent.py b/skills/alphaear-predictor/scripts/prompts/intent_agent.py deleted file mode 100644 index a8397d2..0000000 --- a/skills/alphaear-predictor/scripts/prompts/intent_agent.py +++ /dev/null @@ -1,45 +0,0 @@ -def get_intent_analysis_instructions() -> str: - """生成意图分析 Agent 的系统指令,专注于金融市场影响分析""" - return """你是一个资深的金融市场意图分析专家。你的任务是将用户的自然语言查询转化为结构化的 JSON 分析结果,重点挖掘该查询与金融市场(尤其是股市)的潜在关联。 - -### 核心任务: -深入分析用户查询,识别核心金融实体、行业板块及潜在的市场影响点,生成利于搜索引擎抓取深度金融分析信息的查询词。 - -### 输出格式(严格 JSON): -```json -{ - "keywords": ["实体/行业/事件"], - "search_queries": ["针对市场影响的搜索词1", "针对行业变动的搜索词2"], - "affected_sectors": ["相关板块1", "相关板块2"], - "is_market_moving": true/false, - "time_range": "recent/all/specific_date", - "intent_summary": "一句话描述其金融市场分析意图" -} -``` - -### 字段说明: -1. **keywords**: 核心公司实体、所属行业、宏观经济事件或政策概念。 -2. **search_queries**: 优化后的搜索词,必须包含“股市影响”、“股价波动”、“行业逻辑”或“估值”等金融维度。 -3. **affected_sectors**: 可能受此事件或信息影响的二级市场板块(如:保险、半导体、房地产)。 -4. **is_market_moving**: 该事件是否具有显著的市场驱动潜力或属于重大基本面变化。 -5. **intent_summary**: 简述用户查询背后的金融研究目的。 - -### 示例: -用户输入:"帮我研究一下香港火灾的影响" -输出: -```json -{ - "keywords": ["香港", "火灾", "保险行业", "房地产"], - "search_queries": ["香港火灾对当地保险股股价影响", "香港大火对相关上市物业公司估值冲击", "近期香港火灾带来的市场避险情绪分析"], - "affected_sectors": ["保险", "房地产", "物业管理"], - "is_market_moving": true, - "time_range": "recent", - "intent_summary": "评估香港近期火灾对相关板块上市公司的潜在经济损失及股价冲击" -} -``` -""" - -def get_intent_task(query: str) -> str: - """生成意图分析任务描述""" - return f"Process this query and extract financial market intent: {query}" - diff --git a/skills/alphaear-predictor/scripts/prompts/isq_prompt_generator.py b/skills/alphaear-predictor/scripts/prompts/isq_prompt_generator.py deleted file mode 100644 index 007461b..0000000 --- a/skills/alphaear-predictor/scripts/prompts/isq_prompt_generator.py +++ /dev/null @@ -1,43 +0,0 @@ -""" -ISQ prompt helpers to render dimension guidance directly from the template. -Any change in the template propagates to prompts automatically. -""" - -from typing import List, Optional -from ..schema.isq_template import get_isq_template, ISQTemplate - - -def _ordered_dimension_keys(template: ISQTemplate, order: Optional[List[str]] = None) -> List[str]: - if order: - return [k for k in order if k in template.dimensions] - # fallback to template insertion order - return list(template.dimensions.keys()) - - -def generate_isq_prompt_section(template_id: str = "default_isq_v1", order: Optional[List[str]] = None, include_header: bool = True) -> str: - """Render ISQ dimension text block based on the template. - This allows prompt text to stay in sync with template edits. - """ - template = get_isq_template(template_id) - keys = _ordered_dimension_keys(template, order) - - lines: List[str] = [] - if include_header: - lines.append("### 1. ISQ 评估框架 (Investment Signal Quality)") - lines.append(f"参考模板: {template.template_name} (id: {template.template_id})") - lines.append("") - lines.append("你需要对信号进行以下维度的评分:") - lines.append("") - - for idx, key in enumerate(keys, start=1): - spec = template.dimensions[key] - examples = ";".join([f"{k}: {v}" for k, v in spec.examples.items()]) if spec.examples else "" - lines.append(f"{idx}. **{spec.key} ({spec.name})**: {spec.range_type}") - lines.append(f" - 描述: {spec.description}") - if spec.scale_factor and spec.scale_factor != 1.0: - lines.append(f" - 缩放因子: {spec.scale_factor}") - if examples: - lines.append(f" - 示例: {examples}") - lines.append("") - - return "\n".join(lines).rstrip() diff --git a/skills/alphaear-predictor/scripts/prompts/report_agent.py b/skills/alphaear-predictor/scripts/prompts/report_agent.py deleted file mode 100644 index 6f25c3f..0000000 --- a/skills/alphaear-predictor/scripts/prompts/report_agent.py +++ /dev/null @@ -1,415 +0,0 @@ -# src/prompts/report_agent.py -from datetime import datetime -from typing import Optional -from .isq_prompt_generator import generate_isq_prompt_section - -def get_report_planner_base_instructions() -> str: - """生成报告策划员 (Planner) 的基础系统指令""" - return """你是一名资深的金融研报主编。你的任务是规划报告的结构,将零散的信号聚类成有逻辑的主题。 -你拥有 RAG 搜索工具,可以检索已生成的章节内容以确保逻辑连贯性。 -在规划时,应重点关注信号之间的关联性、产业链的完整性以及用户特定的关注点。""" - -def get_report_writer_base_instructions() -> str: - """生成报告撰写员 (Writer) 的基础系统指令""" - return """你是一名资深金融分析师。你的任务是根据策划员提供的信号簇撰写深度研报章节。 -你应当运用专业的金融知识,将信号转化为深刻的洞察。 -注意:你没有外部搜索工具,你的分析必须基于提供给你的信号内容和行情数据。""" - -def get_report_editor_base_instructions() -> str: - """生成报告编辑 (Editor) 的基础系统指令""" - return """你是一名严谨的金融研报编辑。你的任务是审核和润色撰写员生成的章节。 -你拥有 RAG 搜索工具,可以检索其他章节的内容,以消除重复、修正逻辑冲突并确保术语一致性。 -你应当确保报告符合专业的金融写作规范,且标题层级正确。""" - -# 1. 策划阶段 (Structural Planning) -def format_signal_for_report(signal: any, index: int, cite_keys: Optional[list] = None) -> str: - """格式化单个信号供研报生成使用""" - # 这里的逻辑从 ReportAgent._format_signal_input 迁移过来 - from ..schema.models import InvestmentSignal - - if isinstance(signal, dict): - try: - sig_obj = InvestmentSignal(**signal) - except: - return f"--- 信号 [{index}] ---\n标题: {signal.get('title')}\n内容: {signal.get('content', '')[:500]}" - else: - sig_obj = signal - - chain_str = " -> ".join([f"{n.node_name}({n.impact_type})" for n in sig_obj.transmission_chain]) - - text = f"--- 信号 [{index}] ---\n" - text += f"标题: {sig_obj.title}\n" - text += f"逻辑摘要: {sig_obj.summary}\n" - text += f"传导链条: {chain_str}\n" - text += f"ISQ 评分: 情绪({sig_obj.sentiment_score}), 确定性({sig_obj.confidence}), 强度({sig_obj.intensity})\n" - text += f"预期博弈: 时窗({sig_obj.expected_horizon}), 预期差({sig_obj.price_in_status})\n" - - tickers = ", ".join([f"{t.get('name')}({t.get('ticker')})" for t in sig_obj.impact_tickers]) - if tickers: - text += f"受影响标的: {tickers}\n" - - # Stable bibliography-style citation keys (LaTeX/BibTeX-like) - if cite_keys: - joined = " ".join([f"[@{k}]" for k in cite_keys if k]) - if joined: - text += f"引用: {joined}\n" - - return text - -def get_cluster_planner_instructions(signals_text: str, user_query: str = None) -> str: - """生成信号聚类指令 - 将零散信号组织成逻辑主题""" - query_context = f"用户重点关注:{user_query}" if user_query else "" - return f"""你是一位资深的金融研报主编。你的任务是将以下零散的金融信号聚类成 3-5 个核心逻辑主题,以便撰写一份结构清晰的研报。 - - {query_context} - - ### 输入信号列表 - {signals_text} - - ### 聚类要求 - 1. **主题聚合**: 将相关性强的信号归为一组(例如:都涉及“建筑安全法规”或“某产业链上下游”)。 - 2. **叙事逻辑**: 只需要生成主题名称和包含的信号 ID。 - 3. **控制数量**: 将所有信号归类到 3-5 个主要主题中,不要遗漏。 - - ### 输出格式 (JSON) - 请仅输出以下 JSON 格式,不要包含 Markdown 标记: - {{ - "clusters": [ - {{ - "theme_title": "主题名称(如:建筑安全法规收紧引发的产业链重构)", - "signal_ids": [1, 3, 5], - "rationale": "这些信号都指向政府对高层建筑防火标准的政策调整..." - }}, - ... - ] - }} - """ - -def get_report_planner_instructions(toc: str, signal_count: int, user_query: str = None) -> str: - """生成报告规划指令 - 重点在于逻辑关联与分歧识别""" - # ... (原有逻辑保持不变,但实际在新的聚类流程后这个可能作为备用或二次优化) - query_context = f"用户重点关注:{user_query}" if user_query else "" - return f"""你是一位资深的金融研报主编。你的任务是根据现有的草稿章节,规划出一份逻辑严密、穿透力强的终稿结构。 - - ### 任务核心: - 1. **识别主线**: 从草稿中识别出贯穿多个章节的“核心逻辑主线”(如:产业链共振、货币政策转向)。 - 2. **分歧评估 (Entropy)**: 识别各章节中观点冲突或确定性不一之处,规划如何在正文中呈现这些“分歧点”。 - 3. **结构蓝图**: - - 定义一级标题(逻辑主题)。 - - 归类章节:哪些信号应放入同一主题下深度解析? - - 排序:将 ISQ 强度最高、与{query_context}最相关的信号置前。 - - ### 现有草稿目录 (TOC) - {toc} - - 请输出你的【终稿修订大纲】(Markdown 格式)。 - """ - -# 2. 撰写阶段 (Section Writing) -def get_report_writer_instructions(theme_title: str, signal_cluster_text: str, signal_indices: list, price_context: str = "", user_query: str = None) -> str: - """生成 Writer Agent 指令 - 基于主题聚类撰写综合分析""" - - price_info = f"\n### 近期价格参考\n{price_context}\n" if price_context else "" - query_context = f"\n**用户意图**: \"{user_query}\"\n请确保分析内容回应了用户的关注点。\n" if user_query else "" - isq_block = generate_isq_prompt_section(include_header=False) - - # Keep citation scheme stable across re-ordering / edits. - # Cite keys are provided in each signal block as: 引用: [@KEY] - - return f"""你是一位资深金融分析师。请针对核心主题 **"{theme_title}"** 撰写一篇深度研报章节。 - {query_context} - - ### 输入信号集 (本章节需综合的信号) - {signal_cluster_text} - {price_info} - - ### ISQ 评分说明 - {isq_block} - - ### 写作要求 - 1. **叙事逻辑**: 不要罗列信号,要将这些信号编织成一个连贯的故事。先讲宏观/行业背景,再讲具体事件传导,最后落脚到个股/标的影响。 - 2. **量化支撑**: 引用 ISQ 评分(确定性、强度、预期差)来佐证你的观点。关键观点必须关联相应的 ISQ 分值。 - 3. **引用规范(稳定 CiteKey)**: 关键论断必须标注来源引用,使用 `[@CITE_KEY]` 格式。 - - CiteKey 已在输入信号块中以 `引用: [@KEY]` 提供,请直接复制使用。 - - 不要使用 `[[1]]` 这类不稳定编号。 - 4. **关联标的预测**: **必须**在章节末尾明确给出受影响标的的预测分析,包括: - - 至少列出 1-2 个相关上市公司代码(如 600519.SH) - - 给出短期(T+3或T+5)的方向性判断 - - 如果可能,给出预期价格区间或涨跌幅预测 - - ### 【重要】标题层级规范 - - ❌ **错误示例**(绝对不要这样): - ```markdown - # {theme_title} - - ### 宏观背景 - ... - ``` - - ✅ **正确示例**(必须这样): - ```markdown - ## {theme_title} - - ### 宏观背景 - - 近期全球经济环境... - - ### 具体传导机制分析 - - ... - - ### 核心标的分析 - - 建议关注:贵州茅台(600519.SH)... - ``` - - **关键要求**: - - 章节主标题使用 `##` (H2) - - 章节子标题使用 `###` (H3) - - **绝对禁止**使用 `#` (H1) - - 第一行必须是 `## {theme_title}` 开头 - - ### 核心:图表叙事 (Visual Storytelling) - **必须**在文中插入至少 1-2 个图表,且图表必须与上下文紧密结合(不要堆砌在末尾)。 - - ### 宏观背景 - ... - ``` - - ✅ **正确示例**(必须这样): - ```markdown - ## {theme_title} - - ### 宏观背景 - - 近期全球经济环境... - - ### 具体传导机制分析 - - ... - - ### 核心标的分析 - - 建议关注:贵州茅台(600519.SH)... - ``` - - **关键要求**: - - 章节主标题使用 `##` (H2) - - 章节子标题使用 `###` (H3) - - **绝对禁止**使用 `#` (H1) - - 第一行必须是 `## {theme_title}` 开头 - - ### 核心:图表叙事 (Visual Storytelling) - **必须**在文中插入至少 1-2 个图表,且图表必须与上下文紧密结合(不要堆砌在末尾)。 - - **可选图表类型 (请根据内容选择最合适的 1-2 种):** - - **A. AI 预测 + 走势 (Forecast) - 【强烈推荐 / 最新规范】** - *适用*: 当文中明确提及某上市公司时,**必须**使用此图表展示股价走势与 AI 预测。 - *必填字段*: - - `ticker`: 股票代码,A股 6 位 / 港股 5 位,允许带后缀(如 "002371.SZ"、"9868.HK") - - `pred_len`: 预测交易日长度(建议 3 或 5) - *代码示例*: - ```json-chart - {{"type": "forecast", "ticker": "002371.SZ", "title": "北方华创(002371)T+5 预测", "pred_len": 5}} - ``` - **重要**:禁止手写 `prediction` 数组(预测由系统自动生成并渲染)。 - *注意*: 如果提及多只股票,应为每只生成独立的 forecast 图表。 - - **【推荐写法:多情景 → 最终归因 → 产出唯一预测图】** - 你可以在正文里描述多种情景(如:基准/乐观/悲观),但在插入预测图之前,必须明确给出“本报告最终选择的最可能情景”及其归因,然后用 `forecast` 图表做最终总结。 - 为了让系统把“最终归因”可靠地传递给预测模块,请在 `forecast` JSON 中可选补充以下字段(字段均为可选,越完整越好): - - `selected_scenario`: 最可能情景名称(如 "基准" / "乐观" / "悲观") - - `selection_reason`: 选择该情景的归因理由(1-3 句) - - `scenarios`: 情景列表(数组),每个元素可包含 `name`、`description`、`probability`(0-1) - *示例*: - ```json-chart - {{ - "type": "forecast", - "ticker": "002371.SZ", - "title": "北方华创(002371)T+5 预测(基准情景)", - "pred_len": 5, - "selected_scenario": "基准", - "selection_reason": "结合订单能见度与行业景气,基准情景概率最高;短期扰动主要来自估值与市场风险偏好。", - "scenarios": [ - {{"name": "乐观", "description": "国产替代与资本开支超预期", "probability": 0.25}}, - {{"name": "基准", "description": "订单稳健、利润率小幅波动", "probability": 0.55}}, - {{"name": "悲观", "description": "需求回落或交付节奏放缓", "probability": 0.20}} - ] - }} - ``` - - **B. 历史走势 (Stock) - 仅作为兼容兜底** - *适用*: 当你无法给出预测时(例如无法确定标的),可仅展示历史走势。 - *代码示例*: - ```json-chart - {{"type": "stock", "ticker": "002371", "title": "北方华创历史走势"}} - ``` - - **C. 舆情情绪演变 (Sentiment Trend)** - *适用*: 当讨论行业政策、突发事件(如“火灾”、“新规”)的民意变化时。 - *注意*: `keywords` 必须是事件核心词。 - *代码*: - ```json-chart - {{"type": "sentiment", "keywords": ["建筑安全", "防火标准"], "title": "市场对防火新规的情绪演变"}} - ``` - - **D. 逻辑传导链条 (Transmission Chain)** - *适用*: 复杂的蝴蝶效应分析(支持分支结构)。 - *代码*: - ```json-chart - {{ - "type": "transmission", - "nodes": [ - {{"node_name": "突发火灾", "impact_type": "中性", "logic": "事件发端"}}, - {{"node_name": "监管收紧", "impact_type": "利空", "logic": "合规成本上升", "source": "突发火灾"}}, - {{"node_name": "设备升级", "impact_type": "利好", "logic": "采购需求释放", "source": "突发火灾"}}, - {{"node_name": "龙头受益", "impact_type": "利好", "logic": "市占率提升", "source": "设备升级"}} - ], - "title": "火灾事件的逻辑传导与分支" - }} - ``` - *说明*: 使用 `source` 字段指定父节点名称以创建分支结构。 - - **E. 信号质量评估 (ISQ Radar)** - *适用*: 对某个关键信号进行多维度(确定性、预期差等)定性评估时。 - *代码*: - ```json-chart - {{"type": "isq", "sentiment": 0.8, "confidence": 0.9, "intensity": 4, "expectation_gap": 0.7, "timeliness": 0.9, "title": "核心信号质量评估"}} - ``` - """ - -# 3. 整合阶段 (Final Assembly) - 原版,保留用于 fallback -def get_report_editor_instructions(draft_sections: str, plan: str, sources_list: str) -> str: - """生成最终编辑指令 - 根据规划蓝图重组内容""" - return f"""你是一位专业的研报编辑。请将以下基于主题撰写的草稿章节整合成最终研报。 - - ### 原始草稿内容 - {draft_sections} - - ### 原始引用来源 - {sources_list} - - ### 任务与要求 - 1. **结构化**: 为每个草稿章节添加合适的 Markdown 标题 (## 级别)。 - 2. **连贯性**: 确保章节之间过渡自然。 - 3. **完整性**: - - 必须保留所有 `json-chart` 代码块(图表配置)。 - - 必须保留引用标注 `[@CITE_KEY]`。 - - 生成 `## 核心观点摘要`、`## 参考文献` 和 `## 风险提示`。 - - ### 输出 - 只输出最终的 Markdown 研报内容。 - """ - - -# 4. 单节编辑 (Incremental Section Editing with RAG) -def get_section_editor_instructions(section_index: int, total_sections: int, toc: str) -> str: - """生成单节编辑 prompt,支持 RAG 工具调用""" - return f"""你是一位研报编辑。你正在编辑报告的第 {section_index}/{total_sections} 节。 - - ### 当前目录 (TOC) - {toc} - - ### 你的任务 - 1. 润色当前章节内容,确保逻辑清晰、语言专业。 - 2. 保留所有 `[@CITE_KEY](#ref-CITE_KEY)` 或 `[@CITE_KEY]` 格式的引用。 - 3. 保留所有 `json-chart` 代码块,不做修改。 - 4. 如果需要参考其他章节内容,使用 `search_context` 工具搜索。 - 5. 只输出编辑后的章节内容,不要输出其他章节。 - - ### 【关键】标题层级规范 - **严格遵守以下规则:** - - 章节主标题使用 `##` (H2) - - 章节子标题使用 `###` (H3) - - **禁止使用** `#` (H1) - 只有报告大标题可以使用 H1 - - 如果原文中有 H1,必须将其降级为 H2 - - 不要输出与 "参考文献"、"风险提示" 相同的标题 - - 直接输出编辑后的 Markdown 内容。 - """ - - -# 5. 摘要生成 (Summary Generation) -def get_summary_generator_instructions(toc: str, section_summaries: str) -> str: - """生成报告摘要指令 - 包含市场分歧度分析""" - return f"""你是一位资深研报主笔。请生成今日报告的核心观点摘要的**正文内容**。 - - ### 章节摘要 - {section_summaries} - - ### 任务: - 1. **核心逻辑提炼**: 用 150 字以内总结今日最核心的投资主线。 - 2. **分歧识别**: 如果不同信号对同一板块有冲突观点,请明确指出"市场分歧点"。 - 3. **确定性排序**: 标记出今日确定性最高的前两个机会(需列出具体标的代码)。 - - ### 【重要】输出格式规范: - - ❌ **错误示例**(不要遗漏二级标题): - ```markdown - ### 核心逻辑提炼 - ... - ``` - - ✅ **正确示例**(应该这样输出): - ```markdown - ## 核心观点摘要 - - ### 核心逻辑提炼 - - 科技自立战略加速半导体设备国产化,叠加AI算力需求爆发... - - ### 市场分歧点 - - 资本市场波动显示医药、新能源等板块估值逻辑受政策敏感性增强... - - ### 确定性排序 - - 1. **网络安全替代需求**(ISQ确定性0.85,推荐标的:深信服 300454.SZ) - 2. **半导体设备材料**(ISQ确定性0.75,推荐标的:北方华创 002371.SZ) - ``` - - ### 关键要求: - - 第一行必须是 `## 核心观点摘要` - - 主体部分使用 H3 (`###`) 和 H4 (`####`) 级别标题 - - **必须**包含 `## 核心观点摘要` 这一级标题 - - 现在请按照正确示例的格式输出摘要内容。 - """ - - -# 6. 最终组装 (Final Assembly with Sections) -def get_final_assembly_instructions(sources_list: str) -> str: - """生成最终报告组装的 prompt""" - return f"""你是一位研报主笔。请完成以下任务: - - ### 任务 - 1. 生成 "## 参考文献" 章节(需要按照顺序,顺序不对时进行调整): - - 原始来源: - {sources_list} - - 格式:`[@CITE_KEY] 标题 (来源), [链接地址]` - 2. 生成 "## 风险提示" (标准免责声明)。 - 3. 生成 "## 快速扫描" 表格,汇总各主题的核心观点。 - - 表格列:**主题**, **核心观点**, **强度(Intensity)**, **确定性(Confidence)**。 - - 强度和确定性请参考原章节中的 ISQ 评分。 - - 只输出上述三个章节的 Markdown 内容。 - """ - -def get_cluster_task(signals_preview: str) -> str: - """生成聚类任务描述""" - return f"请对以下信号进行主题聚类:\n\n{signals_preview}" - -def get_writer_task(theme_title: str) -> str: - """生成撰写任务描述""" - return f"请依据主题 '{theme_title}' 和 输入信号集 开始撰写深度分析章节。" - -def get_planner_task() -> str: - """生成规划任务描述""" - return "请阅读现有草稿并规划终稿大纲,识别核心逻辑主线和市场分歧点。" - -def get_editor_task() -> str: - """生成编辑任务描述""" - return "请根据规划大纲和草稿内容,生成最终研报。确保逻辑连贯,保留所有图表和引用。" - diff --git a/skills/alphaear-predictor/scripts/prompts/trend_agent.py b/skills/alphaear-predictor/scripts/prompts/trend_agent.py deleted file mode 100644 index 54e6e22..0000000 --- a/skills/alphaear-predictor/scripts/prompts/trend_agent.py +++ /dev/null @@ -1,156 +0,0 @@ -from typing import Any -from datetime import datetime -from .isq_prompt_generator import generate_isq_prompt_section - -def get_trend_scanner_instructions() -> str: - """生成趋势扫描员 (Scanner) 的系统指令""" - current_time = datetime.now().strftime('%Y-%m-%d %H:%M:%S') - return f"""你是一名专业的数据扫描员,当前时间是 {current_time}。 -你的任务是利用各种工具从互联网和数据库中获取最新的金融新闻、热点趋势和市场数据。 - -### 1. 核心职责 -1. **多源采集**: 使用 `news_toolkit` 获取最新新闻,使用 `stock_toolkit` 获取行情,使用 `polymarket_toolkit` 获取预测市场数据。 -2. **情绪感知**: 使用 `sentiment_toolkit` 对关键新闻进行情绪分析。 -3. **深度搜索**: 针对模糊的热点,使用 `search_toolkit` 进行全网搜索补充细节。 - -### 2. 工具使用规范 -- **广度优先**: 尽可能覆盖多个数据源。 -- **数据新鲜度**: 优先获取最近 24 小时内的信息。 -- **结构化输出**: 整理搜集到的原始数据,为后续评估提供清晰的素材。 -""" - -def get_trend_evaluator_instructions() -> str: - """生成趋势评估员 (Evaluator) 的系统指令""" - current_time = datetime.now().strftime('%Y-%m-%d %H:%M:%S') - isq_block = generate_isq_prompt_section(include_header=True) - - return f""" - 你是一名顶级的金融情报专家 (TrendAgent),擅长从海量信息中识别具有深度价值的"二级市场投资信号"。 - 当前时间:{current_time} - - ### 核心使命: - 不仅是发现"热点",更要解析"信号"。你需要识别那些能触发**传导链条 (Transmission Chain)** 且具有**高确定性 (Confidence)** 的事件。 - - {isq_block} - - ### 核心能力与标准: - 1. **信号识别 (Signal Discovery)**: 基于扫描员提供的素材,识别具有投资价值的信号。优先关注政策、产业变革、重大诉求及跨境套利机会。 - 2. **逻辑相干性**: 是否具备清晰的"原因-结果"传导? - 3. **影响力系数**: 是否会引发板块性的联动或财务指标的实质性扰动? - 4. **市场认知差**: 市场是否已提前消化(Price-in)?寻找尚未被充分交易的"Alpha"。 - 5. **实体穿透**: 必须关联到具体的 Ticker 或核心产业链节点。 - - ### 严禁事项: - - 严禁编造数据。 - - 严禁仅输出情绪极性(Positive/Negative),必须带有逻辑依据。 - - 严禁将纯娱乐或单纯的社会负面事件(除非具有宏观破坏性)视为金融信号。 - - ### 输出要求: - 你发现的每个信号应包含: - - **核心摘要**: 穿透表象的逻辑总结。 - - **传导节点**: A -> B -> C 的逻辑推导。 - - **推荐关注**: 板块或 Ticker。 - - **ISQ 评估**: 基于模板的 5 个维度进行初步评分(具体评分由后续 FinAgent 完成)。 - """ - -def get_trend_agent_instructions() -> str: - # 保持兼容性 - return get_trend_evaluator_instructions() - -def get_trend_scan_task(task_description: str) -> str: - """生成扫描员的任务描述""" - return f"请根据以下任务描述,搜集相关的原始数据和新闻:\n\n{task_description}" - -def format_scan_context(scan_data: dict) -> str: - """将扫描员搜集的结构化数据格式化为评估员可读的文本""" - if not scan_data: - return "(未能搜集到原始数据)" - - return f""" -### 扫描数据概览 -- **热点话题**: {', '.join(scan_data.get('hot_topics', []))} -- **情绪概览**: {scan_data.get('sentiment_overview', '未知')} -- **关键新闻**: {len(scan_data.get('news_summaries', []))} 条 -- **数据摘要**: {scan_data.get('raw_data_summary', '无')} -""" - -def get_trend_eval_task(task_description: str, raw_data_str: str) -> str: - """生成评估员的任务描述""" - return f"""请基于以下搜集到的原始数据,完成最终的分析任务: - -任务描述: {task_description} - -原始数据: -{raw_data_str} - -请识别出最具金融价值的信号,并给出评估理由。""" - -def get_news_filter_instructions(news_count: int, depth: Any, user_query: str = None) -> str: - """生成新闻筛选 prompt,使用 FilterResult schema 加快推理并减少 token 消耗 - - Args: - news_count: 输入新闻总数 - depth: 目标筛选数量,若为 auto 则由 LLM 自主判断 - user_query: 用户输入的查询/关注点(可选) - """ - - # 1. 深度控制逻辑 - if str(depth).lower() == 'auto': - depth_guide = "的数量不设固定限制(建议 3-10 条),根据新闻含金量自动判断" - limit_instruction = "宁缺毋滥,如果高价值信息很少,可以只选 1-2 条;如果都很重要,可以多选。" - else: - try: - d_int = int(depth) - depth_guide = f"约 {d_int} 条" - limit_instruction = f"请尽量凑满 {d_int} 条,但如果剩余新闻全是噪音,则不必强行凑数。" - except: - depth_guide = "适量" - limit_instruction = "根据内容价值判断。" - - target_desc = f"筛选出最具投资分析价值的新闻({depth_guide})。" - - # 2. 用户意图逻辑 - query_instruction = "" - if user_query: - target_desc = f"筛选出与用户意图【{user_query}】最相关的新闻。" - query_instruction = f""" - ### 核心任务(High Priority): - 用户明确关注:"{user_query}"。 - 1. **第一优先级**:必须包含所有与"{user_query}"直接或间接相关的新闻,不要遗漏。 - - 即使这些新闻看起来"价值不高",只要相关都要保留。 - 2. **第二优先级**:在满足第一优先级后,如果名额未满,再补充其他重大的市场热点。 - """ - - return f"""你是一名专业的金融情报精排师。你需要从给定的 {news_count} 条原始新闻流中,{target_desc} - - {query_instruction} - - ### FSD (Financial Signal Density) 筛选准则: - 1. **逻辑传导性 (Transmission)**: 该新闻是否预示着一个明确的产业链传导逻辑?(如:上游涨价 -> 中游成本压力 -> 下游提价预期) - 2. **预期差 (Alpha Potential)**: 是否包含尚未被市场充分Price-in的新突发情况? - 3. **确定性 (Confidence)**: 信息来源是否权威?是否包含具体的财务数据、订单金额或明确的政策日期? - 4. **排除噪音**: 坚决剔除明星八卦、鸡汤文、以及无实质增量的"口号式"新闻。 - - ### {limit_instruction} - - ### 快速有效性检查(TOKEN 优化): - 在开始详细筛选前,先快速判断:这 {news_count} 条新闻中是否至少包含 1 条有效的金融信号? - - 如果全是无关内容(如体育、娱乐、纯生活信息),直接返回 "has_valid_signals": false - - 如果有至少 1 条金融相关的新闻,再进行详细 FSD 筛选 - - ### 输出格式(必须为 JSON,使用 FilterResult schema): - ```json - {{ - "has_valid_signals": true/false, - "selected_ids": ["id_1", "id_2", ...], - "themes": [ - {{ - "name": "高概括性主题", - "news_ids": ["相关id_1", ...], - "fsd_reason": "基于 FSD 准则的筛选理由,重点描述传导逻辑和预期差。" - }} - ], - "reason": "如果 has_valid_signals=false,简要说明原因。否则可为空。" - }} - ``` - """ diff --git a/skills/alphaear-predictor/scripts/prompts/visualizer.py b/skills/alphaear-predictor/scripts/prompts/visualizer.py deleted file mode 100644 index f0b2933..0000000 --- a/skills/alphaear-predictor/scripts/prompts/visualizer.py +++ /dev/null @@ -1,47 +0,0 @@ -def get_drawio_system_prompt(): - return """You are an expert at creating Draw.io (MxGraph) diagrams in XML format. -Your task is to generate a valid MXGraphModel XML based on the user's description. - -### Rules: -1. Output ONLY the XML code. Start with and end with . -2. Do not use compressed XML. Use plain XML. -3. Use standard shapes: 'rounded=1;whiteSpace=wrap;html=1;' for boxes. -4. Auto-layout Strategy: - - Identify "layers" or "stages" in the logic. - - Assign X coordinates based on layers (e.g., 0, 200, 400). - - Assign Y coordinates to distribute nodes vertically (e.g., 0, 100, 200). - - Ensure nodes do not overlap. -5. Edges: Connect nodes logically using . - -### Template: - - - - - - - - - - - - - - - - -""" - -def get_drawio_task(nodes_data: list, title: str) -> str: - import json - nodes_json = json.dumps(nodes_data, ensure_ascii=False, indent=2) - return f"""Please generate a Draw.io XML diagram for the following logic flow: - -**Title**: {title} - -**Nodes and Logic**: -{nodes_json} - -Ensure the layout flows logically from Left to Right (or Top to Bottom for hierarchies). -Use different colors for 'Positive' (Greenish), 'Negative' (Reddish), and 'Neutral' (Grey/Blue) impacts if described. -""" diff --git a/skills/alphaear-predictor/scripts/schema/isq_template.py b/skills/alphaear-predictor/scripts/schema/isq_template.py deleted file mode 100644 index 2709019..0000000 --- a/skills/alphaear-predictor/scripts/schema/isq_template.py +++ /dev/null @@ -1,381 +0,0 @@ -""" -ISQ (Investment Signal Quality) 评估框架 Template - -统一定义 ISQ 的各个维度、评分标准、和使用方法。 -支持默认 template 和自定义 template。 -""" - -from typing import Dict, List, Any, Optional -from pydantic import BaseModel, Field -from enum import Enum -from pathlib import Path -import json - - -class ISQDimension(str, Enum): - """ISQ 评估维度""" - SENTIMENT = "sentiment" # 情绪/走势方向 - CONFIDENCE = "confidence" # 确定性/可信度 - INTENSITY = "intensity" # 强度/影响量级 - EXPECTATION_GAP = "expectation_gap" # 预期差/市场认知差 - TIMELINESS = "timeliness" # 时效性/窗口紧迫度 - TRANSMISSION = "transmission" # 逻辑传导清晰度 - - -class ISQDimensionSpec(BaseModel): - """ISQ 单个维度的定义规范""" - name: str = Field(..., description="维度名称") - key: str = Field(..., description="维度键名") - description: str = Field(..., description="维度描述") - range_type: str = Field(default="0-1", description="取值范围 (0-1 或 1-5 等)") - scale_factor: float = Field(default=1.0, description="显示时的缩放因子") - examples: Dict[str, str] = Field(default_factory=dict, description="不同分值的示例解释") - visualization_color: Optional[str] = Field(default=None, description="可视化颜色") - - -class ISQTemplate(BaseModel): - """ISQ 评估框架 Template""" - template_id: str = Field(..., description="模板 ID") - template_name: str = Field(..., description="模板名称") - description: str = Field(..., description="模板描述") - - # 核心维度定义 - dimensions: Dict[str, ISQDimensionSpec] = Field(..., description="维度定义字典") - - # 评分指导 - scoring_guide: str = Field(..., description="评分指导说明") - - # 应用场景 - applicable_scenarios: List[str] = Field(default_factory=list, description="适用场景") - - # 聚合算法 - aggregation_method: str = Field(default="weighted_average", description="聚合方法 (weighted_average, product 等)") - dimension_weights: Dict[str, float] = Field(default_factory=dict, description="维度权重") - - -class ISQScore(BaseModel): - """单个信号的 ISQ 评分结果""" - signal_id: str = Field(..., description="信号 ID") - template_id: str = Field(..., description="使用的模板 ID") - - # 各维度评分 - scores: Dict[str, float] = Field(..., description="各维度评分") - - # 总分 - overall_score: float = Field(..., description="综合评分") - - # 评分理由 - rationale: Dict[str, str] = Field(default_factory=dict, description="各维度评分理由") - - # 时间戳 - timestamp: str = Field(..., description="评分时间") - - -# ===================================================== -# 默认 Template -# ===================================================== - -DEFAULT_ISQ_TEMPLATE = ISQTemplate( - template_id="default_isq_v1", - template_name="标准投资信号质量评估框架 (ISQ v1.0)", - description="AlphaEar 默认的 ISQ 评估框架,用于标准化评估投资信号的质量维度", - - dimensions={ - "sentiment": ISQDimensionSpec( - name="情绪/走势", - key="sentiment", - description="基础情绪偏向和市场走势判断", - range_type="-1.0 到 1.0", - scale_factor=1.0, - examples={ - "-1.0": "极度悲观/极度看空", - "-0.5": "明显看空", - "0.0": "中性/没有明确方向", - "0.5": "明显看多", - "1.0": "极度乐观/极度看多" - }, - visualization_color="#ef4444" # 红色表示负面,绿色表示正面 - ), - - "confidence": ISQDimensionSpec( - name="确定性", - key="confidence", - description="信号的可信度和确定性程度", - range_type="0.0 到 1.0", - scale_factor=1.0, - examples={ - "0.0-0.3": "信息来源不可靠/传言多/逻辑推导牵强", - "0.3-0.6": "信息相对可靠/有一定逻辑/但仍有不确定性", - "0.6-0.8": "信息来源权威/逻辑清晰/高度可信", - "0.8-1.0": "官方确认/数据明确/完全确定" - }, - visualization_color="#3b82f6" # 蓝色 - ), - - "intensity": ISQDimensionSpec( - name="强度/影响量级", - key="intensity", - description="信号对相关板块/个股的潜在影响程度", - range_type="1 到 5", - scale_factor=20.0, # 用于雷达图缩放 (5 -> 100) - examples={ - "1": "影响微弱,可能被市场忽略", - "2": "小幅影响,短期可能有波动", - "3": "中等影响,值得重点关注", - "4": "强烈影响,可能成为市场焦点", - "5": "极强影响,市场预期明显变化" - }, - visualization_color="#f97316" # 橙色 - ), - - "expectation_gap": ISQDimensionSpec( - name="预期差", - key="expectation_gap", - description="市场预期与现实之间的差距", - range_type="0.0 到 1.0", - scale_factor=1.0, - examples={ - "0.0-0.2": "市场充分认知,预期差小", - "0.2-0.5": "市场部分认知,存在一定预期差", - "0.5-0.8": "市场认知不足,预期差较大,存在博弈空间", - "0.8-1.0": "市场严重低估/高估,巨大预期差" - }, - visualization_color="#22c55e" # 绿色 - ), - - "timeliness": ISQDimensionSpec( - name="时效性", - key="timeliness", - description="信号的时间窗口紧迫度", - range_type="0.0 到 1.0", - scale_factor=1.0, - examples={ - "0.0-0.2": "长期信号,反应窗口 > 3 月", - "0.2-0.5": "中期信号,反应窗口 1-3 月", - "0.5-0.8": "短期信号,反应窗口 1 周 - 1 月", - "0.8-1.0": "超短期信号,反应窗口 < 1 周(需立即行动)" - }, - visualization_color="#a855f7" # 紫色 - ), - }, - - scoring_guide=""" - ### ISQ 评分指导 (Investment Signal Quality) - - ISQ 框架用于多维度评估投资信号的质量。每个信号由 5 个维度组成: - - 1. **情绪 (Sentiment)**: -1.0 到 1.0,表示看空(-)/中性(0)/看多(+) - 2. **确定性 (Confidence)**: 0.0 到 1.0,数值越高越确定 - 3. **强度 (Intensity)**: 1 到 5,数值越高影响越大 - 4. **预期差 (Expectation Gap)**: 0.0 到 1.0,市场预期与现实的差距 - 5. **时效性 (Timeliness)**: 0.0 到 1.0,反应窗口的紧迫程度 - - ### 综合评分算法 - - 综合评分 = 确定性 × 0.35 + 强度/5 × 0.30 + 预期差 × 0.20 + 时效性 × 0.15 - - 范围: 0.0 到 1.0 - - 0.0-0.3: 信号质量较差,不建议跟进 - - 0.3-0.6: 信号质量一般,可作参考 - - 0.6-0.8: 信号质量良好,值得跟进 - - 0.8-1.0: 信号质量优异,强烈推荐 - - ### 评分时的注意事项 - - - **不要混淆方向和强度**:情绪可以是看空,但确定性和强度仍可能很高 - - **预期差往往是 Alpha 来源**:高预期差 + 高确定性 = 最佳博弈机会 - - **考虑时间成本**:长期信号需要更高的确定性才值得跟进 - - **数据为王**:所有评分必须有具体数据支撑 - """, - - applicable_scenarios=[ - "上市公司基本面变化分析", - "产业政策与监管事件评估", - "地缘政治与宏观经济影响", - "技术进步与产业升级", - "突发事件与应急响应" - ], - - aggregation_method="weighted_average", - dimension_weights={ - "confidence": 0.35, - "intensity": 0.30, - "expectation_gap": 0.20, - "timeliness": 0.15 - } -) - - -# ===================================================== -# ISQ Template 管理系统 -# ===================================================== - -class ISQTemplateManager: - """ISQ Template 管理器""" - - def __init__(self): - self.templates: Dict[str, ISQTemplate] = { - DEFAULT_ISQ_TEMPLATE.template_id: DEFAULT_ISQ_TEMPLATE - } - - def register_template(self, template: ISQTemplate) -> None: - """注册新的 template""" - self.templates[template.template_id] = template - - def register_template_dict(self, template_dict: Dict[str, Any]) -> ISQTemplate: - """从 dict 注册模板,返回实例。""" - tpl = ISQTemplate(**template_dict) - self.register_template(tpl) - return tpl - - def get_template(self, template_id: str) -> ISQTemplate: - """获取指定 template""" - if template_id not in self.templates: - return DEFAULT_ISQ_TEMPLATE - return self.templates[template_id] - - def list_templates(self) -> List[Dict[str, str]]: - """列出所有可用 template""" - return [ - { - "id": t.template_id, - "name": t.template_name, - "description": t.description, - "dimensions": list(t.dimensions.keys()) - } - for t in self.templates.values() - ] - - def get_dimension(self, template_id: str, dimension_key: str) -> ISQDimensionSpec: - """获取指定 template 的某个维度定义""" - template = self.get_template(template_id) - return template.dimensions.get(dimension_key) - - def get_scoring_prompt(self, template_id: str) -> str: - """获取用于 LLM 的评分 prompt""" - template = self.get_template(template_id) - - dimensions_desc = "\n".join([ - f"- **{d.name} ({d.key})**\n" - f" 范围: {d.range_type}\n" - f" 说明: {d.description}\n" - f" 示例: {', '.join(f'{k}={v}' for k, v in list(d.examples.items())[:3])}" - for d in template.dimensions.values() - ]) - - return f""" -### ISQ 评估指导 ({template.template_name}) - -使用以下 {len(template.dimensions)} 个维度评估信号质量: - -{dimensions_desc} - -### 评分标准 -{template.scoring_guide} - -### 输出格式 (JSON) -请输出以下 JSON 格式的评分结果: -{{ - "sentiment": , - "confidence": , - "intensity": , - "expectation_gap": , - "timeliness": , - "rationale": {{ - "sentiment": "评分理由", - "confidence": "评分理由", - "intensity": "评分理由", - "expectation_gap": "评分理由", - "timeliness": "评分理由" - }} -}} -""" - - -# 全局 template 管理器实例 -isq_template_manager = ISQTemplateManager() - - -# ===================================================== -# 配置加载 -# ===================================================== - -def load_templates_from_config(config_path: Optional[str] = None) -> None: - """从配置目录加载所有 JSON 模板文件,未找到则跳过,不影响默认模板。 - 支持单个 JSON 文件或目录(目录下的所有 .json 文件)。 - """ - if config_path: - path = Path(config_path) - else: - # 默认目录:config/isq_templates/ - # __file__ = src/schema/isq_template.py - # parent = src/schema, parent.parent = src, parent.parent.parent = 项目根目录 - path = Path(__file__).resolve().parent.parent.parent / "config" - - if not path.exists(): - return - - # 如果是目录,扫描所有 .json 文件 - if path.is_dir(): - json_files = list(path.glob("*.json")) - else: - json_files = [path] - - for json_file in json_files: - try: - data = json.loads(json_file.read_text(encoding="utf-8")) - - # 如果是单个模板对象,转为列表 - if isinstance(data, dict): - templates = [data] - elif isinstance(data, list): - templates = data - else: - continue - - # 注册所有模板 - for tpl_dict in templates: - if not isinstance(tpl_dict, dict): - continue - try: - isq_template_manager.register_template_dict(tpl_dict) - except Exception: - # 忽略单个模板的加载错误,继续其他模板 - continue - except Exception: - # JSON 解析失败,跳过该文件 - continue - - -# 在模块加载时自动尝试加载配置模板 -load_templates_from_config() - - -# ===================================================== -# 便利函数 -# ===================================================== - -def get_isq_template(template_id: str = "default_isq_v1") -> ISQTemplate: - """获取 ISQ template""" - return isq_template_manager.get_template(template_id) - - -def get_isq_scoring_prompt(template_id: str = "default_isq_v1") -> str: - """获取用于 LLM 的 ISQ 评分 prompt""" - return isq_template_manager.get_scoring_prompt(template_id) - - -def calculate_isq_overall_score(scores: Dict[str, float], template_id: str = "default_isq_v1") -> float: - """计算 ISQ 综合评分""" - template = get_isq_template(template_id) - - overall = 0.0 - for dim_key, weight in template.dimension_weights.items(): - if dim_key in scores: - score = scores[dim_key] - # 处理强度维度的特殊缩放 (1-5 -> 0-1) - if dim_key == "intensity": - score = score / 5.0 - overall += score * weight - - return min(1.0, max(0.0, overall)) # 限制在 0-1 之间 diff --git a/skills/alphaear-predictor/scripts/schema/models.py b/skills/alphaear-predictor/scripts/schema/models.py deleted file mode 100644 index 422ca9c..0000000 --- a/skills/alphaear-predictor/scripts/schema/models.py +++ /dev/null @@ -1,100 +0,0 @@ -from pydantic import BaseModel, Field -from typing import List, Optional, Dict, Any -from datetime import datetime - -class TransmissionNode(BaseModel): - node_name: str = Field(..., description="产业链节点名称") - impact_type: str = Field(..., description="利好/利空/中性") - logic: str = Field(..., description="该节点的传导逻辑") - -class IntentAnalysis(BaseModel): - keywords: List[str] = Field(..., description="核心实体、事件或概念关键词") - search_queries: List[str] = Field(..., description="优化后的搜索引擎查询词") - is_specific_event: bool = Field(..., description="是否查询特定突发事件") - time_range: str = Field(..., description="时间范围 (recent/all/specific_date)") - intent_summary: str = Field(..., description="一句话意图描述") - -class FilterResult(BaseModel): - """LLM 筛选结果 - 快速判断是否有有效信号""" - has_valid_signals: bool = Field(..., description="列表中是否包含有效的金融信号") - selected_ids: List[int] = Field(default_factory=list, description="筛选出的有效信号 ID 列表") - themes: List[str] = Field(default_factory=list, description="信号涉及的主题") - reason: Optional[str] = Field(default=None, description="如果无有效信号,说明原因") - -class InvestmentSignal(BaseModel): - # 核心元数据 - signal_id: str = Field(default="unknown_sig", description="唯一信号 ID") - title: str = Field(..., description="信号标题") - summary: str = Field(default="暂无摘要分析", description="100 字核心观点快报") - reasoning: str = Field(default="", description="详细的推演逻辑和理由") - - # 逻辑传导 (ISQ Key 1) - transmission_chain: List[TransmissionNode] = Field(default_factory=list, description="产业链传导逻辑链条") - - # 信号质量 (ISQ Key 2) - 来自 isq_template.DEFAULT_ISQ_TEMPLATE - # 参考: src/schema/isq_template.py 的 DEFAULT_ISQ_TEMPLATE 定义 - sentiment_score: float = Field(default=0.0, description="[ISQ] 情绪/走势 (-1.0=极度看空 ~ 0.0=中性 ~ 1.0=极度看多)") - confidence: float = Field(default=0.5, description="[ISQ] 确定性 (0.0=不可信 ~ 1.0=完全确定)") - intensity: int = Field(default=3, description="[ISQ] 强度/影响量级 (1=微弱 ~ 5=极强)") - expectation_gap: float = Field(default=0.5, description="[ISQ] 预期差/博弈空间 (0.0=充分定价 ~ 1.0=巨大预期差)") - timeliness: float = Field(default=0.8, description="[ISQ] 时效性 (0.0=长期 ~ 1.0=超短期)") - - # 预测与博弈 (ISQ Key 3) - expected_horizon: str = Field(default="T+N", description="预期的反应时窗 (如: T+0, T+3, Long-term)") - price_in_status: str = Field(default="未知", description="市场预期消化程度 (未定价/部分定价/充分定价)") - - # 关联实体 - impact_tickers: List[Dict[str, Any]] = Field(default_factory=list, description="受影响的代码列表及其权重") - industry_tags: List[str] = Field(default_factory=list, description="关联行业标签") - - # 溯源 - sources: List[Dict[str, str]] = Field(default_factory=list, description="来源详情 (包含 title, url, source_name)") - -class ResearchContext(BaseModel): - """研究员搜集的背景信息结构""" - raw_signal: str = Field(..., description="原始信号内容") - tickers_found: List[Dict[str, Any]] = Field(default_factory=list, description="找到的相关标的及其基本面/股价信息") - industry_background: str = Field(..., description="行业背景及产业链现状") - latest_developments: List[str] = Field(default_factory=list, description="相关事件的最新进展") - key_risks: List[str] = Field(default_factory=list, description="潜在风险点") - search_results_summary: str = Field(..., description="搜索结果的综合摘要") - -class ScanContext(BaseModel): - """扫描员搜集的原始数据结构""" - hot_topics: List[str] = Field(..., description="当前市场热点话题") - news_summaries: List[Dict[str, Any]] = Field(..., description="关键新闻摘要列表") - market_data: Dict[str, Any] = Field(default_factory=dict, description="相关的市场行情数据") - sentiment_overview: str = Field(..., description="整体市场情绪概览") - raw_data_summary: str = Field(..., description="原始数据的综合摘要") - -class SignalCluster(BaseModel): - theme_title: str = Field(..., description="主题名称") - signal_ids: List[int] = Field(..., description="包含的信号 ID 列表") - rationale: str = Field(..., description="聚类理由") - -class ClusterContext(BaseModel): - """信号聚类结果结构""" - clusters: List[SignalCluster] = Field(..., description="聚类列表") - -class KLinePoint(BaseModel): - date: str = Field(..., description="日期") - open: float = Field(..., description="开盘价") - high: float = Field(..., description="最高价") - low: float = Field(..., description="最低价") - close: float = Field(..., description="收盘价") - volume: float = Field(..., description="成交量") - -class ForecastResult(BaseModel): - ticker: str = Field(..., description="股票代码") - base_forecast: List[KLinePoint] = Field(default_factory=list, description="Kronos 模型原始预测") - adjusted_forecast: List[KLinePoint] = Field(default_factory=list, description="LLM 调整后的预测") - rationale: str = Field(default="", description="预测调整理由及逻辑说明") - timestamp: str = Field(default_factory=lambda: datetime.now().strftime("%Y-%m-%d %H:%M:%S"), description="生成时间") - -class InvestmentReport(BaseModel): - overall_sentiment: str = Field(..., description="整体市场情绪评价") - market_entropy: float = Field(..., description="市场分歧度 (0-1, 1代表极高分歧)") - signals: List[InvestmentSignal] = Field(..., description="深度解析的投资信号列表") - forecasts: List[ForecastResult] = Field(default_factory=list, description="相关标的的预测结果") - timestamp: str = Field(..., description="报告生成时间") - meta_info: Optional[Dict[str, Any]] = Field(default_factory=dict, description="其他元数据") diff --git a/skills/alphaear-predictor/scripts/utils/__init__.py b/skills/alphaear-predictor/scripts/utils/__init__.py deleted file mode 100644 index 27e1961..0000000 --- a/skills/alphaear-predictor/scripts/utils/__init__.py +++ /dev/null @@ -1 +0,0 @@ -# AlphaEar utils package diff --git a/skills/alphaear-predictor/scripts/utils/database_manager.py b/skills/alphaear-predictor/scripts/utils/database_manager.py deleted file mode 100644 index cfc362b..0000000 --- a/skills/alphaear-predictor/scripts/utils/database_manager.py +++ /dev/null @@ -1,581 +0,0 @@ -import sqlite3 -import json -from datetime import datetime, date -from pathlib import Path -from typing import List, Dict, Optional, Any, Union -import pandas as pd -from loguru import logger - -class DatabaseManager: - """ - AlphaEar 数据库管理器 - 负责存储热点数据、搜索缓存和股价数据 - 使用 SQLite 进行持久化存储 - """ - - def __init__(self, db_path: str = "data/signal_flux.db"): - self.db_path = Path(db_path) - self.db_path.parent.mkdir(parents=True, exist_ok=True) - self.conn = sqlite3.connect(str(self.db_path), check_same_thread=False) - self.conn.row_factory = sqlite3.Row - self._init_db() - logger.info(f"💾 Database initialized at {self.db_path}") - - def _init_db(self): - """初始化表结构""" - cursor = self.conn.cursor() - - # 1. 每日热点新闻表 - cursor.execute(""" - CREATE TABLE IF NOT EXISTS daily_news ( - id TEXT PRIMARY KEY, - source TEXT, - rank INTEGER, - title TEXT, - url TEXT, - content TEXT, - publish_time TEXT, - crawl_time TEXT, - sentiment_score REAL, - analysis TEXT, - meta_data TEXT - ) - """) - - # 尝试添加 analysis 列(如果表已存在但没有该列) - try: - cursor.execute("ALTER TABLE daily_news ADD COLUMN analysis TEXT") - except: - pass # 列已存在 - - - # 2. 搜索缓存表 (原有 JSON 缓存) - cursor.execute(""" - CREATE TABLE IF NOT EXISTS search_cache ( - query_hash TEXT PRIMARY KEY, - query TEXT, - engine TEXT, - results TEXT, - timestamp TEXT - ) - """) - - # 2.5 搜索详情表 (展开的搜索结果) - cursor.execute(""" - CREATE TABLE IF NOT EXISTS search_detail ( - id TEXT, - query_hash TEXT, - rank INTEGER, - title TEXT, - url TEXT, - content TEXT, - publish_time TEXT, - crawl_time TEXT, - sentiment_score REAL, - source TEXT, - meta_data TEXT, - PRIMARY KEY (query_hash, id) - ) - """) - - # 3. 股价数据表 - cursor.execute(""" - CREATE TABLE IF NOT EXISTS stock_prices ( - ticker TEXT, - date TEXT, - open REAL, - close REAL, - high REAL, - low REAL, - volume REAL, - change_pct REAL, - PRIMARY KEY (ticker, date) - ) - """) - - # 4. 股票列表表 (用于检索) - cursor.execute(""" - CREATE TABLE IF NOT EXISTS stock_list ( - code TEXT PRIMARY KEY, - name TEXT - ) - """) - - # 5. 投资信号表 (ISQ Framework) - cursor.execute(""" - CREATE TABLE IF NOT EXISTS signals ( - signal_id TEXT PRIMARY KEY, - title TEXT, - summary TEXT, - transmission_chain TEXT, - sentiment_score REAL, - confidence REAL, - intensity INTEGER, - expected_horizon TEXT, - price_in_status TEXT, - impact_tickers TEXT, - industry_tags TEXT, - sources TEXT, - user_id TEXT, - created_at TEXT - ) - """) - - - - # 6. 创建索引以优化查询性能 - cursor.execute("CREATE INDEX IF NOT EXISTS idx_news_crawl_time ON daily_news(crawl_time)") - cursor.execute("CREATE INDEX IF NOT EXISTS idx_news_source ON daily_news(source)") - cursor.execute("CREATE INDEX IF NOT EXISTS idx_search_cache_timestamp ON search_cache(timestamp)") - cursor.execute("CREATE INDEX IF NOT EXISTS idx_stock_prices_ticker_date ON stock_prices(ticker, date)") - # 尝试添加 user_id 列到 signals 表 - try: - cursor.execute("ALTER TABLE signals ADD COLUMN user_id TEXT") - except: - pass - - cursor.execute("CREATE INDEX IF NOT EXISTS idx_signals_user_id ON signals(user_id)") - - self.conn.commit() - - # - # self.conn.commit() - - - # --- 新闻数据操作 --- - - def save_daily_news(self, news_list: List[Dict]) -> int: - """保存热点新闻,包含发布时间与抓取时间""" - cursor = self.conn.cursor() - count = 0 - crawl_time = datetime.now().isoformat() - - for news in news_list: - try: - # 兼容不同来源的 ID 生成逻辑 - news_id = news.get('id') or f"{news.get('source')}_{news.get('rank')}_{crawl_time[:10]}" - cursor.execute(""" - INSERT OR REPLACE INTO daily_news - (id, source, rank, title, url, content, publish_time, crawl_time, sentiment_score, meta_data) - VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?) - """, ( - news_id, - news.get('source'), - news.get('rank'), - news.get('title'), - news.get('url'), - news.get('content', ''), - news.get('publish_time'), # 新增支持发布时间 - crawl_time, - news.get('sentiment_score'), - json.dumps(news.get('meta_data', {})) - )) - count += 1 - except sqlite3.Error as e: - logger.error(f"Database error saving news item {news.get('title')}: {e}") - except Exception as e: - logger.error(f"Unexpected error saving news item {news.get('title')}: {e}") - - self.conn.commit() - return count - - def get_daily_news(self, source: Optional[str] = None, limit: int = 100, days: int = 1) -> List[Dict]: - """获取最近 N 天的热点新闻""" - cursor = self.conn.cursor() - # 使用 crawl_time 过滤,保证结果的新鲜度 - time_threshold = (datetime.now().timestamp() - days * 86400) - time_threshold_str = datetime.fromtimestamp(time_threshold).isoformat() - - query = "SELECT * FROM daily_news WHERE crawl_time >= ?" - params = [time_threshold_str] - - if source: - query += " AND source = ?" - params.append(source) - - query += " ORDER BY crawl_time DESC, rank LIMIT ?" - params.append(limit) - - cursor.execute(query, params) - return [dict(row) for row in cursor.fetchall()] - - def lookup_reference_by_url(self, url: str) -> Optional[Dict[str, Any]]: - """Best-effort lookup of a source item by URL. - - This is used to render a stable bibliography from DB-backed metadata. - It searches both `daily_news` and `search_detail`. - """ - url = (url or "").strip() - if not url: - return None - - cursor = self.conn.cursor() - - try: - cursor.execute( - """ - SELECT title, source, publish_time, crawl_time, url - FROM daily_news - WHERE url = ? - ORDER BY crawl_time DESC - LIMIT 1 - """, - (url,), - ) - row = cursor.fetchone() - if row: - return dict(row) - except Exception: - pass - - try: - cursor.execute( - """ - SELECT title, source, publish_time, crawl_time, url - FROM search_detail - WHERE url = ? - ORDER BY crawl_time DESC - LIMIT 1 - """, - (url,), - ) - row = cursor.fetchone() - if row: - return dict(row) - except Exception: - pass - - return None - - def delete_news(self, news_id: str) -> bool: - """删除特定新闻""" - cursor = self.conn.cursor() - cursor.execute("DELETE FROM daily_news WHERE id = ?", (news_id,)) - self.conn.commit() - return cursor.rowcount > 0 - - def update_news_content(self, news_id: str, content: str = None, analysis: str = None) -> bool: - """更新新闻的内容或分析结果""" - cursor = self.conn.cursor() - updates = [] - params = [] - - if content is not None: - updates.append("content = ?") - params.append(content) - if analysis is not None: - updates.append("analysis = ?") - params.append(analysis) - - if not updates: - return False - - params.append(news_id) - query = f"UPDATE daily_news SET {', '.join(updates)} WHERE id = ?" - cursor.execute(query, params) - self.conn.commit() - return cursor.rowcount > 0 - - # --- 搜索缓存辅助 --- - - def get_search_cache(self, query_hash: str, ttl_seconds: Optional[int] = None) -> Optional[Dict]: - """获取搜索缓存 (优先查 search_detail)""" - cursor = self.conn.cursor() - - # 1. 尝试从 search_detail 获取展开的结构化数据 - cursor.execute(""" - SELECT * FROM search_detail - WHERE query_hash = ? - ORDER BY rank - """, (query_hash,)) - details = [dict(row) for row in cursor.fetchall()] - - if details: - # 检查 TTL (取第一条的时间) - first_time = datetime.fromisoformat(details[0]['crawl_time']) - if ttl_seconds and (datetime.now() - first_time).total_seconds() > ttl_seconds: - logger.info(f"⌛ Detailed cache expired for hash {query_hash}") - pass # Expired, fall through or return None? If Detail expired, Cache likely expired too. - # But let's check basic cache just in case metadata differs? - # Actually if details exist, we prefer them. If expired, we return None. - return None - - logger.info(f"✅ Hit detailed search cache for {query_hash} ({len(details)} items)") - # Reconstruct the expected 'results' list format for SearchTools - # SearchTools expects a list of dicts. - # We return a dict wrapper to match get_search_cache signature returning Dict usually containing 'results' string. - # But SearchTools logic: - # cache = db.get_search_cache(...) - # cached_data = json.loads(cache['results']) - - # To minimize SearchTools changes, we can return a dict mimicking the old structure - # OR Change SearchTools to handle list return. - # Let's return a special dict that SearchTools can recognize or just format it as before. - return {"results": json.dumps(details), "timestamp": details[0]['crawl_time']} - - # 2. Fallback to old table - cursor.execute("SELECT * FROM search_cache WHERE query_hash = ?", (query_hash,)) - row = cursor.fetchone() - - if not row: - return None - - row_dict = dict(row) - if ttl_seconds: - cache_time = datetime.fromisoformat(row_dict['timestamp']) - if (datetime.now() - cache_time).total_seconds() > ttl_seconds: - logger.info(f"⌛ Cache expired for hash {query_hash}") - return None - - return row_dict - - def save_search_cache(self, query_hash: str, query: str, engine: str, results: Union[str, List[Dict]]): - """保存搜索结果 (同时保存到 search_cache 和 search_detail)""" - cursor = self.conn.cursor() - current_time = datetime.now().isoformat() - - results_str = results if isinstance(results, str) else json.dumps(results) - - # 1. Save summary to search_cache - cursor.execute(""" - INSERT OR REPLACE INTO search_cache (query_hash, query, engine, results, timestamp) - VALUES (?, ?, ?, ?, ?) - """, (query_hash, query, engine, results_str, current_time)) - - # 2. Save details to search_detail if results is a list - if isinstance(results, list): - for item in results: - try: - item_id = item.get('id') or f"{hash(item.get('url', ''))}" - cursor.execute(""" - INSERT OR REPLACE INTO search_detail - (id, query_hash, rank, title, url, content, publish_time, crawl_time, sentiment_score, source, meta_data) - VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?) - """, ( - str(item_id), - query_hash, - item.get('rank', 0), - item.get('title'), - item.get('url'), - item.get('content', ''), - item.get('publish_time'), - item.get('crawl_time') or current_time, - item.get('sentiment_score'), - item.get('source'), - json.dumps(item.get('meta_data', {})) - )) - except sqlite3.Error as e: - logger.error(f"Database error saving search detail {item.get('title')}: {e}") - except Exception as e: - logger.error(f"Unexpected error saving search detail {item.get('title')}: {e}") - - self.conn.commit() - - def find_similar_queries(self, query: str, limit: int = 5) -> List[Dict]: - """模糊搜索相似的已缓存查询""" - cursor = self.conn.cursor() - - # Simple fuzzy match: query in cached OR cached in query - q_wild = f"%{query}%" - cursor.execute(""" - SELECT query, query_hash, timestamp, results - FROM search_cache - WHERE query LIKE ? OR ? LIKE ('%' || query || '%') - ORDER BY timestamp DESC - LIMIT ? - """, (q_wild, query, limit)) - - return [dict(row) for row in cursor.fetchall()] - - def search_local_news(self, query: str, limit: int = 5) -> List[Dict]: - """从本地 daily_news 搜索相关新闻""" - cursor = self.conn.cursor() - q_wild = f"%{query}%" - # Search title and content - cursor.execute(""" - SELECT * FROM daily_news - WHERE title LIKE ? OR content LIKE ? - ORDER BY crawl_time DESC - LIMIT ? - """, (q_wild, q_wild, limit)) - return [dict(row) for row in cursor.fetchall()] - - # --- 股票数据操作 --- - - def save_stock_list(self, df: pd.DataFrame): - """保存股票列表到 stock_list 表""" - cursor = self.conn.cursor() - try: - # 清空旧表 - cursor.execute("DELETE FROM stock_list") - - # 批量插入 - data = df[['code', 'name']].to_dict('records') - cursor.executemany( - "INSERT INTO stock_list (code, name) VALUES (:code, :name)", - data - ) - self.conn.commit() - except sqlite3.Error as e: - logger.error(f"Database error saving stock list: {e}") - except Exception as e: - logger.error(f"Unexpected error saving stock list: {e}") - - def search_stock(self, query: str, limit: int = 5) -> List[Dict]: - """模糊搜索股票代码或名称""" - cursor = self.conn.cursor() - wild = f"%{query}%" - cursor.execute(""" - SELECT code, name FROM stock_list - WHERE code LIKE ? OR name LIKE ? - LIMIT ? - """, (wild, wild, limit)) - return [dict(row) for row in cursor.fetchall()] - - def get_stock_by_code(self, code: str) -> Optional[Dict[str, str]]: - """精确按代码获取股票信息。 - - Args: - code: 股票代码(A股6位 / 港股5位),必须为纯数字字符串。 - - Returns: - dict: {"code": str, "name": str} 或 None。 - """ - if not code: - return None - clean = "".join([c for c in str(code).strip() if c.isdigit()]) - if not clean: - return None - - cursor = self.conn.cursor() - cursor.execute("SELECT code, name FROM stock_list WHERE code = ? LIMIT 1", (clean,)) - row = cursor.fetchone() - return dict(row) if row else None - - def save_stock_prices(self, ticker: str, df: pd.DataFrame): - """保存股价历史数据""" - if df.empty: - return - - cursor = self.conn.cursor() - - # 确保 DataFrame 有必要的列 - required_cols = ['date', 'open', 'close', 'high', 'low', 'volume', 'change_pct'] - for col in required_cols: - if col not in df.columns: - logger.warning(f"Missing column {col} in stock data for {ticker}") - return - - try: - for _, row in df.iterrows(): - cursor.execute(""" - INSERT OR REPLACE INTO stock_prices - (ticker, date, open, close, high, low, volume, change_pct) - VALUES (?, ?, ?, ?, ?, ?, ?, ?) - """, ( - ticker, - row['date'], - row['open'], - row['close'], - row['high'], - row['low'], - row['volume'], - row['change_pct'] - )) - self.conn.commit() - except sqlite3.Error as e: - logger.error(f"Database error saving stock prices for {ticker}: {e}") - except Exception as e: - logger.error(f"Unexpected error saving stock prices for {ticker}: {e}") - - def get_stock_prices(self, ticker: str, start_date: str, end_date: str) -> pd.DataFrame: - """获取指定日期范围的股价数据""" - cursor = self.conn.cursor() - - cursor.execute(""" - SELECT * FROM stock_prices - WHERE ticker = ? AND date >= ? AND date <= ? - ORDER BY date - """, (ticker, start_date, end_date)) - - rows = cursor.fetchall() - if not rows: - return pd.DataFrame() - - columns = ['ticker', 'date', 'open', 'close', 'high', 'low', 'volume', 'change_pct'] - return pd.DataFrame([dict(row) for row in rows], columns=columns) - - def execute_query(self, query: str, params: tuple = ()) -> List[Any]: - """执行自定义 SQL 查询""" - try: - cursor = self.conn.cursor() - cursor.execute(query, params) - if query.strip().upper().startswith("SELECT"): - return cursor.fetchall() - else: - self.conn.commit() - return [] - except sqlite3.Error as e: - logger.error(f"SQL execution failed (Database error): {e}") - return [] - except Exception as e: - logger.error(f"SQL execution failed (Unexpected error): {e}") - return [] - - # --- 投资信号操作 (ISQ Framework) --- - - def save_signal(self, signal: Dict[str, Any]): - """保存投资信号""" - cursor = self.conn.cursor() - created_at = datetime.now().isoformat() - - cursor.execute(""" - INSERT OR REPLACE INTO signals - (signal_id, title, summary, transmission_chain, sentiment_score, - confidence, intensity, expected_horizon, price_in_status, - impact_tickers, industry_tags, sources, user_id, created_at) - VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?) - """, ( - signal.get('signal_id'), - signal.get('title'), - signal.get('summary'), - json.dumps(signal.get('transmission_chain', [])), - signal.get('sentiment_score', 0.0), - signal.get('confidence', 0.0), - signal.get('intensity', 1), - signal.get('expected_horizon', 'T+0'), - signal.get('price_in_status', '未知'), - json.dumps(signal.get('impact_tickers', [])), - json.dumps(signal.get('industry_tags', [])), - json.dumps(signal.get('sources', [])), - signal.get('user_id'), - created_at - )) - self.conn.commit() - - def get_recent_signals(self, limit: int = 20, user_id: Optional[str] = None) -> List[Dict]: - """获取最近的投资信号""" - cursor = self.conn.cursor() - if user_id: - cursor.execute("SELECT * FROM signals WHERE user_id = ? ORDER BY created_at DESC LIMIT ?", (user_id, limit)) - else: - cursor.execute("SELECT * FROM signals ORDER BY created_at DESC LIMIT ?", (limit,)) - rows = cursor.fetchall() - - signals = [] - for row in rows: - d = dict(row) - # 解析 JSON 字段 - for field in ['transmission_chain', 'impact_tickers', 'industry_tags', 'sources']: - if d.get(field): - try: - d[field] = json.loads(d[field]) - except: - pass - signals.append(d) - return signals - - def close(self): - if self.conn: - self.conn.close() - logger.info("Database connection closed.") - diff --git a/skills/alphaear-predictor/scripts/utils/json_utils.py b/skills/alphaear-predictor/scripts/utils/json_utils.py deleted file mode 100644 index c29aab2..0000000 --- a/skills/alphaear-predictor/scripts/utils/json_utils.py +++ /dev/null @@ -1,180 +0,0 @@ -import ast -import json -import re -from typing import Optional, Any -from loguru import logger - -def _strip_comments(text: str) -> str: - """ - Safely remove C-style comments (// and /* */) from JSON-like text, - preserving strings (including URLs like http://). - """ - result = [] - i = 0 - n = len(text) - in_string = False - escape = False - - while i < n: - char = text[i] - - if in_string: - if char == '\\': - escape = not escape - elif char == '"' and not escape: - in_string = False - else: - escape = False - result.append(char) - i += 1 - continue - - # Not in string - if char == '"': - in_string = True - result.append(char) - i += 1 - continue - - # Check for // comment - if i + 1 < n and text[i:i+2] == '//': - i += 2 - while i < n and text[i] != '\n': - i += 1 - continue - - # Check for /* comment - if i + 1 < n and text[i:i+2] == '/*': - i += 2 - while i + 1 < n and text[i:i+2] != '*/': - i += 1 - i += 2 - continue - - result.append(char) - i += 1 - - return ''.join(result) - -def extract_json(text: str) -> Optional[Any]: - """ - 更加鲁棒的 JSON 提取工具。 - 处理: - 1. Markdown 代码块 (```json ... ```) - 2. 首尾多余字符 - 3. 同一个文本中多个 JSON 对象 (仅提取第一个) - 4. 简单的 JSON 修复 (末尾逗号等) - 5. C 风格注释 (// 和 /* */) - """ - if not text: - return None - - # 1. 清理明显的 Markdown 包装 - text = text.strip() - - # 先尝试精确匹配 ```json ... ``` 或 ```...``` - md_match = re.search(r'```(?:json)?\s*\n?(.*?)\n?```', text, re.DOTALL) - if md_match: - text = md_match.group(1).strip() - elif text.startswith("```"): - # 回退:如果开头有 ``` 但没完整匹配 - text = re.sub(r'^```[a-z]*\n?', '', text) - text = re.sub(r'\n?```\s*$', '', text) - - # 2. 寻找第一个 JSON 起始符 { 或 [ - start_brace = text.find('{') - start_bracket = text.find('[') - - if start_brace == -1 and start_bracket == -1: - return None - - start_idx = start_brace if (start_bracket == -1 or (start_brace != -1 and start_brace < start_bracket)) else start_bracket - - # 2.5 预处理:修复一些极其常见的 LLM 错误 - potential_json = text[start_idx:].strip() - - # remove comments safely - potential_json = _strip_comments(potential_json) - - # b. 修复缺失开头引号的键: nodes": [ -> "nodes": [ - # 匹配模式: (空白或换行) 单词 紧跟引号和冒号 - potential_json = re.sub(r'([\{\,]\s*)([a-zA-Z_]\w*)\"\s*:', r'\1"\2":', potential_json) - - # c. 修复缺失末尾引号的键: "nodes: [ -> "nodes": [ - potential_json = re.sub(r'([\{\,]\s*)\"([a-zA-Z_]\w*)\s*:', r'\1"\2":', potential_json) - - # d. 修复完全缺失引号的键: nodes: [ -> "nodes": [ - # 注意避免匹配到像 http:// 这种内容,所以限定在 { 或 , 之后 - potential_json = re.sub(r'([\{\,]\s*)([a-zA-Z_]\w*)\s*:', r'\1"\2":', potential_json) - - # 3. 使用 raw_decode 尝试解析 - decoder = json.JSONDecoder() - - # 首先尝试直接解析(不做任何预处理) - try: - obj = json.loads(potential_json) - return obj - except json.JSONDecodeError: - pass - - # 简单预处理:移除对象/列表末位多余逗号 - processed_json = re.sub(r',\s*([\]}])', r'\1', potential_json) - - try: - obj, end_pos = decoder.raw_decode(processed_json) - return obj - except json.JSONDecodeError: - pass - - # e. 修复未终止的字符串字面量问题:移除值中的实际换行符 - # LLM 可能在字符串值中生成包含真实 newline 的内容,导致 JSON 非法 - def fix_multiline_strings(s): - # 简单策略:将字符串值内的换行替换为空格 - lines = s.split('\n') - result = [] - in_string = False - for line in lines: - # 计算未转义的引号数 - quote_count = line.count('"') - line.count('\\"') - if in_string: - result[-1] += ' ' + line.strip() - else: - result.append(line) - - if quote_count % 2 == 1: - in_string = not in_string - return '\n'.join(result) - - fixed_json = fix_multiline_strings(processed_json) - - try: - obj, end_pos = decoder.raw_decode(fixed_json) - return obj - except json.JSONDecodeError: - try: - # 4. 尝试处理单引号问题 (JSON 规范要求双引号,但 LLM 常输出单引号) - # 这是一个简单的替换技巧,仅针对像 {'key': 'value'} 这样的结构 - # 注意:这可能会破坏包含单引号的字符串值,所以作为较后的回退 - fix_quotes = re.sub(r"'(.*?)':", r'"\1":', processed_json) # 修复键 - fix_quotes = re.sub(r":\s*'(.*?)'", r': "\1"', fix_quotes) # 修复简单值 - obj, end_pos = decoder.raw_decode(fix_quotes) - return obj - except (json.JSONDecodeError, TypeError): - try: - # 5. 使用 ast.literal_eval 作为终极回退 (处理 Python 字典格式) - # 提取第一个匹配的括号对内容 - # 寻找匹配的 { } - stack = [] - for i, char in enumerate(potential_json): - if char == '{': stack.append('{') - elif char == '}': - if stack: stack.pop() - if not stack: - content = potential_json[:i+1] - return ast.literal_eval(content) - except (ValueError, SyntaxError, MemoryError) as e: - logger.warning(f"All JSON extraction attempts failed: {e}") - except Exception as e: - logger.error(f"Unexpected error during JSON extraction: {e}") - - return None diff --git a/skills/alphaear-predictor/scripts/utils/llm/capability.py b/skills/alphaear-predictor/scripts/utils/llm/capability.py deleted file mode 100644 index d07ca4f..0000000 --- a/skills/alphaear-predictor/scripts/utils/llm/capability.py +++ /dev/null @@ -1,85 +0,0 @@ -import os -from typing import Optional, List, Dict, Any -from agno.agent import Agent -from agno.models.base import Model -from loguru import logger -from ..llm.factory import get_model - - -def test_tool_call_support(model: Model) -> bool: - """ - 测试模型是否支持原生的 Tool Call (Function Calling)。 - 通过尝试执行一个简单的加法工具来验证。 - """ - - def get_current_weather(location: str): - """获取指定地点的天气""" - return f"{location} 的天气是晴天,25度。" - - test_agent = Agent( - model=model, - tools=[get_current_weather], - instructions="请调用工具查询北京的天气,并直接返回工具的输出结果。", - ) - - try: - # 运行一个简单的任务,观察是否触发了 tool_call - response = test_agent.run("北京天气怎么样?") - - # 检查 response 中是否包含 tool_calls - # Agno 的 RunResponse 对象通常包含 messages,我们可以检查最后几条消息 - has_tool_call = False - for msg in response.messages: - if hasattr(msg, "tool_calls") and msg.tool_calls: - has_tool_call = True - break - - if has_tool_call: - logger.info(f"✅ Model {model.id} supports native tool calling.") - return True - else: - # 如果没有 tool_calls 但返回了正确答案,可能是模型通过纯文本模拟了工具调用(ReAct) - # 或者根本没用工具。对于原生支持的判断,我们坚持要求有 tool_calls 结构。 - logger.warning( - f"⚠️ Model {model.id} did NOT use native tool calling structure." - ) - return False - - except Exception as e: - logger.error(f"❌ Error testing tool call for {model.id}: {e}") - return False - - -class ModelCapabilityRegistry: - """ - 模型能力注册表,用于缓存和管理不同模型的能力测试结果。 - """ - - _cache = {} - - @classmethod - def get_capabilities( - cls, provider: str, model_id: str, **kwargs - ) -> Dict[str, bool]: - key = f"{provider}:{model_id}" - if key not in cls._cache: - logger.info(f"🔍 Testing capabilities for {key}...") - model = get_model(provider, model_id, **kwargs) - supports_tool_call = test_tool_call_support(model) - cls._cache[key] = {"supports_tool_call": supports_tool_call} - return cls._cache[key] - - -if __name__ == "__main__": - import os - from skills._env_loader import load_unified_env - - load_unified_env() - - # 测试当前配置的模型 - p = os.getenv("LLM_PROVIDER", "minimax") - m = os.getenv("LLM_MODEL", "Qwen") - - print(f"Testing {p}/{m}...") - res = ModelCapabilityRegistry.get_capabilities(p, m) - print(f"Result: {res}") diff --git a/skills/alphaear-predictor/scripts/utils/llm/factory.py b/skills/alphaear-predictor/scripts/utils/llm/factory.py deleted file mode 100644 index 449e5b8..0000000 --- a/skills/alphaear-predictor/scripts/utils/llm/factory.py +++ /dev/null @@ -1,122 +0,0 @@ -import os -from agno.models.openai import OpenAIChat -from agno.models.ollama import Ollama -from agno.models.dashscope import DashScope -from agno.models.deepseek import DeepSeek -from agno.models.openrouter import OpenRouter - - -def get_model(model_provider: str, model_id: str, **kwargs): - """ - Factory to get the appropriate LLM model. - - Args: - model_provider: "openai", "ollama", "deepseek" - model_id: The specific model ID (e.g., "gpt-4o", "llama3", "deepseek-chat") - **kwargs: Additional arguments for the model constructor - """ - if model_provider == "openai": - return OpenAIChat(id=model_id, **kwargs) - - elif model_provider == "ollama": - return Ollama(id=model_id, **kwargs) - - elif model_provider == "minimax": - api_key = os.getenv("MINIMAX_API_KEY") - if not api_key: - print("Warning: MINIMAX_API_KEY not set.") - - return OpenAIChat( - id=model_id, - base_url=os.getenv("MINIMAX_API_BASE", "https://api.minimax.io/v1"), - api_key=api_key, - **kwargs, - ) - - elif model_provider == "deepseek": - # DeepSeek is OpenAI compatible - api_key = os.getenv("DEEPSEEK_API_KEY") - if not api_key: - print("Warning: DEEPSEEK_API_KEY not set.") - - return DeepSeek(id=model_id, api_key=api_key, **kwargs) - elif model_provider == "dashscope": - api_key = os.getenv("DASHSCOPE_API_KEY") - if not api_key: - print("Warning: DASHSCOPE_API_KEY not set.") - - return DashScope( - id=model_id, - base_url="https://dashscope.aliyuncs.com/compatible-mode/v1", - api_key=api_key, - **kwargs, - ) - elif model_provider == "openrouter": - api_key = os.getenv("OPENROUTER_API_KEY") - if not api_key: - print("Warning: OPENROUTER_API_KEY not set.") - - return OpenRouter(id=model_id, api_key=api_key, **kwargs) - - elif model_provider == "zai": - api_key = os.getenv("ZAI_KEY_API") - if not api_key: - print("Warning: ZAI_KEY_API not set.") - - # role_map to ensure compatibility. - default_role_map = { - "system": "system", - "user": "user", - "assistant": "assistant", - "tool": "tool", - "model": "assistant", - } - - # Allow callers to override role_map via kwargs, otherwise use default - role_map = kwargs.pop("role_map", default_role_map) - - return OpenAIChat( - id=model_id, - base_url="https://api.z.ai/api/paas/v4", - api_key=api_key, - timeout=60, - role_map=role_map, - extra_body={ - "enable_thinking": False - }, # TODO: one more setting for thinking - **kwargs, - ) - - elif model_provider == "ust": - api_key = os.getenv("UST_KEY_API") - if not api_key: - print("Warning: UST_KEY_API not set.") - - # Some UST-compatible endpoints expect the standard OpenAI role names - # (e.g. "system", "user", "assistant") rather than Agno's default - # mapping which maps "system" -> "developer". Provide an explicit - # role_map to ensure compatibility. - default_role_map = { - "system": "system", - "user": "user", - "assistant": "assistant", - "tool": "tool", - "model": "assistant", - } - - # Allow callers to override role_map via kwargs, otherwise use default - role_map = kwargs.pop("role_map", default_role_map) - - return OpenAIChat( - id=model_id, - api_key=api_key, - base_url=os.getenv("UST_URL"), - role_map=role_map, - extra_body={ - "enable_thinking": False - }, # TODO: one more setting for thinking - **kwargs, - ) - - else: - raise ValueError(f"Unknown model provider: {model_provider}") diff --git a/skills/alphaear-predictor/scripts/utils/llm/router.py b/skills/alphaear-predictor/scripts/utils/llm/router.py deleted file mode 100644 index 8c69958..0000000 --- a/skills/alphaear-predictor/scripts/utils/llm/router.py +++ /dev/null @@ -1,80 +0,0 @@ -import os -from typing import Optional, List, Dict, Any, Union -from agno.models.base import Model -from loguru import logger -from ..llm.factory import get_model -from ..llm.capability import ModelCapabilityRegistry -from skills._env_loader import load_unified_env - -load_unified_env() - - -class ModelRouter: - """ - 模型路由管理器 - - 功能: - 1. 管理“推理/写作模型” (Reasoning Model) 和“工具调用模型” (Tool Model)。 - 2. 根据任务需求自动选择合适的模型。 - """ - - def __init__(self): - # 默认从环境变量读取 - self.reasoning_provider = os.getenv( - "REASONING_MODEL_PROVIDER", os.getenv("LLM_PROVIDER", "openai") - ) - self.reasoning_id = os.getenv( - "REASONING_MODEL_ID", os.getenv("LLM_MODEL", "gpt-4o") - ) - self.reasoning_host = os.getenv("REASONING_MODEL_HOST", os.getenv("LLM_HOST")) - - self.tool_provider = os.getenv("TOOL_MODEL_PROVIDER", self.reasoning_provider) - self.tool_id = os.getenv("TOOL_MODEL_ID", self.reasoning_id) - self.tool_host = os.getenv("TOOL_MODEL_HOST", self.reasoning_host) - - self._reasoning_model = None - self._tool_model = None - - logger.info( - f"🤖 ModelRouter initialized: Reasoning={self.reasoning_id} ({self.reasoning_host or 'default'}), Tool={self.tool_id} ({self.tool_host or 'default'})" - ) - - def get_reasoning_model(self, **kwargs) -> Model: - if not self._reasoning_model: - # 优先使用路由配置的 host - if self.reasoning_host and "host" not in kwargs: - kwargs["host"] = self.reasoning_host - self._reasoning_model = get_model( - self.reasoning_provider, self.reasoning_id, **kwargs - ) - return self._reasoning_model - - def get_tool_model(self, **kwargs) -> Model: - if not self._tool_model: - # 优先使用路由配置的 host - if self.tool_host and "host" not in kwargs: - kwargs["host"] = self.tool_host - - # 检查 tool_model 是否真的支持 tool call - caps = ModelCapabilityRegistry.get_capabilities( - self.tool_provider, self.tool_id, **kwargs - ) - if not caps["supports_tool_call"]: - logger.warning( - f"⚠️ Configured tool model {self.tool_id} might not support native tool calls! Consider using ReAct mode or a different model." - ) - - self._tool_model = get_model(self.tool_provider, self.tool_id, **kwargs) - return self._tool_model - - def get_model_for_agent(self, has_tools: bool = False, **kwargs) -> Model: - """ - 根据 Agent 是否包含工具来返回合适的模型。 - """ - if has_tools: - return self.get_tool_model(**kwargs) - return self.get_reasoning_model(**kwargs) - - -# 全局单例 -router = ModelRouter() diff --git a/skills/alphaear-predictor/scripts/utils/logging_setup.py b/skills/alphaear-predictor/scripts/utils/logging_setup.py deleted file mode 100644 index 9a2ca62..0000000 --- a/skills/alphaear-predictor/scripts/utils/logging_setup.py +++ /dev/null @@ -1,45 +0,0 @@ -import os -import sys -from datetime import datetime -from typing import Optional - -from loguru import logger - - -def setup_file_logging( - run_id: str, - log_dir: str = "logs", - level: str = "INFO", - retention: str = "10 days", - rotation: str = "20 MB", -) -> str: - """Configure Loguru to log to stderr + a per-run file. - - Returns the log file path. - """ - os.makedirs(log_dir, exist_ok=True) - - # Remove default handler to avoid duplicate logs. - logger.remove() - - # Console - logger.add(sys.stderr, level=level, backtrace=False, diagnose=False) - - # File (safe for multi-thread via enqueue) - log_path = os.path.join(log_dir, f"signalflux_{run_id}.log") - logger.add( - log_path, - level=level, - rotation=rotation, - retention=retention, - enqueue=True, - backtrace=True, - diagnose=False, - encoding="utf-8", - ) - return log_path - - -def make_run_id(prefix: Optional[str] = None) -> str: - ts = datetime.now().strftime("%Y%m%d_%H%M%S") - return f"{prefix}_{ts}" if prefix else ts diff --git a/skills/alphaear-predictor/scripts/utils/predictor/evaluation.py b/skills/alphaear-predictor/scripts/utils/predictor/evaluation.py deleted file mode 100644 index 26c5df7..0000000 --- a/skills/alphaear-predictor/scripts/utils/predictor/evaluation.py +++ /dev/null @@ -1,137 +0,0 @@ -import os -import sys -import torch -import pandas as pd -import numpy as np -import glob -from loguru import logger -from datetime import datetime, timedelta - -# Setup paths -KRONOS_DIR = os.path.dirname(os.path.abspath(__file__)) -SRC_DIR = os.path.dirname(os.path.dirname(KRONOS_DIR)) -if SRC_DIR not in sys.path: - sys.path.insert(0, SRC_DIR) - -from ..kronos.auto_synthesis_training import AutoSynthesisTrainer -from ..kronos.model import KronosPredictor -from ..visualizer import VisualizerTools -from ..schema.models import ForecastResult, KLinePoint - -class NewsModelEvaluator: - def __init__(self, model_path=None): - self.trainer = AutoSynthesisTrainer() - self.device = self.trainer.device - - if model_path is None: - # Try to find the latest model in exports/models - model_files = glob.glob(os.path.join(SRC_DIR, "exports/models/*.pt")) - if not model_files: - logger.warning("⚠️ No trained models found in exports/models/. Using base model (zero-init proj).") - else: - model_path = max(model_files, key=os.path.getctime) - - if model_path: - self.load_weights(model_path) - - def load_weights(self, path): - logger.info(f"🔄 Loading model weights from {path}...") - checkpoint = torch.load(path, map_location=self.device) - self.trainer.model.news_proj.load_state_dict(checkpoint['news_proj_state_dict']) - logger.success("✅ News projection layer loaded.") - - def evaluate_range(self, start_idx=100, end_idx=200, pred_len=5): - # 1. Fetch Tickers - res = self.trainer.db.execute_query("SELECT code FROM stock_list") - all_tickers = [row['code'] for row in res] - test_tickers = all_tickers[start_idx:end_idx] - - if not test_tickers: - logger.error(f"No tickers found in range {start_idx}-{end_idx}") - return - - logger.info(f"🚀 Evaluating News Model on stocks {start_idx} to {end_idx}...") - - # 2. Discover Shocks - shocks = self.trainer.discover_shocks(test_tickers, pred_len=pred_len) - - # 3. Associate News & Predict - self.trainer.model.eval() - predictor = KronosPredictor(self.trainer.model, self.trainer.tokenizer, device=self.device) - - save_dir = os.path.join(SRC_DIR, "exports/evaluation_results") - os.makedirs(save_dir, exist_ok=True) - - count = 0 - for shock in shocks: - summary = self.trainer.find_reason_and_verify(shock) - if not summary: - continue - - logger.info(f"📈 Testing shock: {shock['ticker']} on {shock['date']}") - - # Embedding news - news_emb = self.trainer.embedder.encode(summary) - - # Prediction - h = shock['history'] - t = shock['target'] - actuals = t['close'].values[:pred_len] - - x_ts = pd.to_datetime(h['date']) - future_dates = pd.date_range(start=x_ts.iloc[-1] + timedelta(days=1), periods=pred_len, freq='B') - y_ts = pd.Series(future_dates) - - # A. Base Prediction (No news) - p_base = predictor.predict(h, x_ts, y_ts, pred_len=pred_len, news_emb=None, verbose=False) - - # B. News-Aware Prediction - p_news = predictor.predict(h, x_ts, y_ts, pred_len=pred_len, news_emb=news_emb, verbose=False) - - # Calculate Improvement - b_preds = p_base['close'].values[:len(actuals)] - n_preds = p_news['close'].values[:len(actuals)] - b_mae = np.mean(np.abs(b_preds - actuals)) - n_mae = np.mean(np.abs(n_preds - actuals)) - improvement = (b_mae - n_mae) / (b_mae + 1e-6) * 100 - - # C. Visualize - try: - def to_kp_list(preds_df): - points = [] - for idx, row in preds_df.iterrows(): - points.append(KLinePoint( - date=str(idx)[:10], open=row['open'], high=row['high'], - low=row['low'], close=row['close'], volume=row.get('volume', 0) - )) - return points - - forecast_obj = ForecastResult( - ticker=shock['ticker'], - base_forecast=to_kp_list(p_base), - adjusted_forecast=to_kp_list(p_news), - rationale=summary - ) - - chart = VisualizerTools.generate_stock_chart( - df=h, ticker=shock['ticker'], - title=f"Test Eval: {shock['ticker']} ({shock['date']}) Imp: {improvement:.1f}%", - forecast=forecast_obj, - ground_truth=t[['date', 'open', 'high', 'low', 'close', 'volume']] - ) - - safe_date = shock['date'].replace("-", "") - filename = f"test_{shock['ticker']}_{safe_date}.html" - VisualizerTools.render_chart_to_file(chart, os.path.join(save_dir, filename)) - - logger.success(f"📊 Result for {shock['ticker']} saved. Base MAE: {b_mae:.4f}, News MAE: {n_mae:.4f}") - count += 1 - except Exception as e: - logger.error(f"Visualization failed: {e}") - - logger.info(f"🏁 Finished evaluation. {count} cases visualized in {save_dir}") - -if __name__ == "__main__": - # If you have a specific model, pass the path here. Otherwise it picks the latest. - evaluator = NewsModelEvaluator() - evaluator.evaluate_range(start_idx=100, end_idx=200, pred_len=1) diff --git a/skills/alphaear-predictor/scripts/utils/predictor/kline_generate.py b/skills/alphaear-predictor/scripts/utils/predictor/kline_generate.py deleted file mode 100644 index 3224c21..0000000 --- a/skills/alphaear-predictor/scripts/utils/predictor/kline_generate.py +++ /dev/null @@ -1,196 +0,0 @@ -# Ref: https://github.com/shiyu-coder/Kronos - -from model import Kronos, KronosTokenizer, KronosPredictor -import pandas as pd -import sqlite3 -import torch -import matplotlib.pyplot as plt -import matplotlib.gridspec as gridspec -from pandas.tseries.offsets import BusinessDay -import numpy as np - -def get_device(): - device = "cuda" if torch.cuda.is_available() else "mps" if torch.backends.mps.is_available() else "cpu" - print(f"Using device: {device}") - return device - -def load_predictor(): - tokenizer = KronosTokenizer.from_pretrained("NeoQuasar/Kronos-Tokenizer-base") - model = Kronos.from_pretrained("NeoQuasar/Kronos-base") - device = get_device() - tokenizer = tokenizer.to(device) - model = model.to(device) - return KronosPredictor(model, tokenizer, device=device, max_context=512) - -def load_data(ticker="002111", db_path="AlphaEar/data/signal_flux.db"): - with sqlite3.connect(db_path) as conn: - df = pd.read_sql_query(f"SELECT * FROM stock_prices WHERE ticker = '{ticker}'", conn) - df['date'] = pd.to_datetime(df['date']) - df = df.sort_values('date').reset_index(drop=True) - return df - -def plot_kline_matplotlib(ax, ax_vol, dates, df, label_suffix="", color_up='#ef4444', color_down='#22c55e', alpha=1.0, is_prediction=False): - """ - 绘制 K 线图和成交量 - """ - # X axis mapping to integers for consistent spacing - x = np.arange(len(dates)) - - # K-line data - opens = df['open'].values - closes = df['close'].values - highs = df['high'].values - lows = df['low'].values - volumes = df['volume'].values - - # Width of the candlestick - width = 0.6 - - for i in range(len(x)): - color = color_up if closes[i] >= opens[i] else color_down - linestyle = '--' if is_prediction else '-' - - # Wick - ax.vlines(x[i], lows[i], highs[i], color=color, linewidth=1, alpha=alpha, linestyle=linestyle) - - # Body - rect_bottom = min(opens[i], closes[i]) - rect_height = abs(opens[i] - closes[i]) - if rect_height == 0: rect_height = 0.001 # Visual hair - - ax.add_patch(plt.Rectangle((x[i] - width/2, rect_bottom), width, rect_height, - edgecolor=color, facecolor=color if not is_prediction else 'none', - alpha=alpha, linewidth=1, linestyle=linestyle)) - - # Volume - ax_vol.bar(x[i], volumes[i], color=color, alpha=alpha * 0.5, width=width) - -def render_comparison_chart(history_df, actual_df, pred_df, title): - """ - 渲染组合图:历史 K 线 + 真值 K 线 + 预测 K 线 - """ - # Combine all dates for X axis - all_dates = pd.concat([history_df['date'], actual_df['date'] if actual_df is not None else pred_df.index.to_series()]).unique() - all_dates = sorted(all_dates) - date_to_idx = {date: i for i, date in enumerate(all_dates)} - - fig = plt.figure(figsize=(14, 8), facecolor='white') - gs = gridspec.GridSpec(2, 1, height_ratios=[3, 1], hspace=0.1) - ax_main = fig.add_subplot(gs[0]) - ax_vol = fig.add_subplot(gs[1], sharex=ax_main) - - # 1. Plot History - hist_indices = [date_to_idx[d] for d in history_df['date']] - # We use a custom x for plotting to ensure continuity - plot_kline_matplotlib(ax_main, ax_vol, history_df['date'], history_df, alpha=0.8) - - offset = len(history_df) - - # 2. Plot Actual if exists - if actual_df is not None: - # Shift indices - actual_x = np.arange(len(actual_df)) + offset - # Plotting manually to handle offset - for i in range(len(actual_df)): - idx = actual_x[i] - row = actual_df.iloc[i] - color = '#ef4444' if row['close'] >= row['open'] else '#22c55e' - ax_main.vlines(idx, row['low'], row['high'], color=color, linewidth=1, alpha=0.9) - ax_main.add_patch(plt.Rectangle((idx - 0.3, min(row['open'], row['close'])), 0.6, abs(row['open']-row['close']), - edgecolor=color, facecolor=color, alpha=0.9)) - ax_vol.bar(idx, row['volume'], color=color, alpha=0.4) - - # 3. Plot Prediction - pred_x = np.arange(len(pred_df)) + offset - for i in range(len(pred_df)): - idx = pred_x[i] - row = pred_df.iloc[i] - color = '#ff8c00' # Orange for prediction to distinguish - ax_main.vlines(idx, row['low'], row['high'], color=color, linewidth=1.5, linestyle='--') - ax_main.add_patch(plt.Rectangle((idx - 0.3, min(row['open'], row['close'])), 0.6, abs(row['open']-row['close']), - edgecolor=color, facecolor='none', linewidth=1.5, linestyle='--')) - # Plot secondary prediction line for close - if i == 0: - # Connect to history - ax_main.plot([offset-1, idx], [history_df['close'].iloc[-1], row['close']], color=color, linestyle='--', alpha=0.6) - elif i > 0: - ax_main.plot([idx-1, idx], [pred_df['close'].iloc[i-1], row['close']], color=color, linestyle='--', alpha=0.6) - - # Styling - ax_main.set_title(title, fontsize=14, fontweight='bold') - ax_main.grid(True, linestyle=':', alpha=0.6) - ax_vol.grid(True, linestyle=':', alpha=0.6) - ax_vol.set_ylabel('Volume') - ax_main.set_ylabel('Price') - - # Set X ticks - step = max(1, len(all_dates) // 10) - ax_vol.set_xticks(np.arange(0, len(all_dates), step)) - ax_vol.set_xticklabels([all_dates[i].strftime('%Y-%m-%d') for i in range(0, len(all_dates), step)], rotation=45) - - plt.tight_layout() - plt.show() - plt.close() - -def run_backtest(df, predictor, lookback, pred_len, start_index=0): - total_len = len(df) - history_start = start_index - history_end = start_index + lookback - pred_start = history_end - - available_pred_len = total_len - pred_start - if available_pred_len <= 0: return - actual_pred_len = min(pred_len, available_pred_len) - pred_end = pred_start + actual_pred_len - - x_df = df.iloc[history_start : history_end].copy() - y_true_df = df.iloc[pred_start : pred_end].copy() - y_timestamp = y_true_df['date'] - - print(f"Backtesting: {x_df['date'].iloc[0].date()} to {y_timestamp.iloc[-1].date()}") - - pred_df = predictor.predict( - df=x_df[['open', 'high', 'low', 'close', 'volume']], - x_timestamp=x_df['date'], - y_timestamp=y_timestamp, - pred_len=actual_pred_len, - T=1.0, top_p=0.9, sample_count=1 - ) - - render_comparison_chart(x_df, y_true_df, pred_df, f"Backtest: {TICKER} K-Line Comparison") - -def run_forecast(df, predictor, lookback, pred_len): - if len(df) < lookback: return - x_df = df.iloc[-lookback:].copy() - last_date = x_df['date'].iloc[-1] - future_dates = pd.date_range(start=last_date + BusinessDay(1), periods=pred_len, freq='B') - future_dates = pd.Series(future_dates) - - print(f"Forecasting: Starting from {future_dates.iloc[0].date()}") - - pred_df = predictor.predict( - df=x_df[['open', 'high', 'low', 'close', 'volume']], - x_timestamp=x_df['date'], - y_timestamp=future_dates, - pred_len=pred_len, - T=1.0, top_p=0.9, sample_count=1 - ) - - render_comparison_chart(x_df, None, pred_df, f"Forecast: {TICKER} Future K-Line") - -if __name__ == "__main__": - LOOKBACK = 20 - PRED_LEN = 10 - TICKER = '002111' - - pred_model = load_predictor() - stock_data = load_data(TICKER) - - total_rows = len(stock_data) - backtest_start = max(0, total_rows - LOOKBACK - PRED_LEN - 10) # Leave some space to see trend - - print("\n--- Running Backtest ---") - run_backtest(stock_data, pred_model, LOOKBACK, PRED_LEN, start_index=backtest_start) - - print("\n--- Running Forecast ---") - run_forecast(stock_data, pred_model, LOOKBACK, PRED_LEN) \ No newline at end of file diff --git a/skills/alphaear-predictor/scripts/utils/predictor/model/__init__.py b/skills/alphaear-predictor/scripts/utils/predictor/model/__init__.py deleted file mode 100644 index d10e200..0000000 --- a/skills/alphaear-predictor/scripts/utils/predictor/model/__init__.py +++ /dev/null @@ -1,16 +0,0 @@ -from .kronos import KronosTokenizer, Kronos, KronosPredictor - -model_dict = { - 'kronos_tokenizer': KronosTokenizer, - 'kronos': Kronos, - 'kronos_predictor': KronosPredictor -} - - -def get_model_class(model_name): - if model_name in model_dict: - return model_dict[model_name] - else: - print(f"Model {model_name} not found in model_dict") - raise NotImplementedError - diff --git a/skills/alphaear-predictor/scripts/utils/predictor/model/kronos.py b/skills/alphaear-predictor/scripts/utils/predictor/model/kronos.py deleted file mode 100644 index cf8bece..0000000 --- a/skills/alphaear-predictor/scripts/utils/predictor/model/kronos.py +++ /dev/null @@ -1,676 +0,0 @@ -import numpy as np -import pandas as pd -import torch -from huggingface_hub import PyTorchModelHubMixin -import sys - -from tqdm import trange - -sys.path.append("../") -from model.module import * - - -class KronosTokenizer(nn.Module, PyTorchModelHubMixin): - """ - KronosTokenizer module for tokenizing input data using a hybrid quantization approach. - - This tokenizer utilizes a combination of encoder and decoder Transformer blocks - along with the Binary Spherical Quantization (BSQuantizer) to compress and decompress input data. - - Args: - d_in (int): Input dimension. - d_model (int): Model dimension. - n_heads (int): Number of attention heads. - ff_dim (int): Feed-forward dimension. - n_enc_layers (int): Number of encoder layers. - n_dec_layers (int): Number of decoder layers. - ffn_dropout_p (float): Dropout probability for feed-forward networks. - attn_dropout_p (float): Dropout probability for attention mechanisms. - resid_dropout_p (float): Dropout probability for residual connections. - s1_bits (int): Number of bits for the pre token in BSQuantizer. - s2_bits (int): Number of bits for the post token in BSQuantizer. - beta (float): Beta parameter for BSQuantizer. - gamma0 (float): Gamma0 parameter for BSQuantizer. - gamma (float): Gamma parameter for BSQuantizer. - zeta (float): Zeta parameter for BSQuantizer. - group_size (int): Group size parameter for BSQuantizer. - - """ - - def __init__(self, d_in, d_model, n_heads, ff_dim, n_enc_layers, n_dec_layers, ffn_dropout_p, attn_dropout_p, resid_dropout_p, s1_bits, s2_bits, beta, gamma0, gamma, zeta, group_size): - - super().__init__() - self.d_in = d_in - self.d_model = d_model - self.n_heads = n_heads - self.ff_dim = ff_dim - self.enc_layers = n_enc_layers - self.dec_layers = n_dec_layers - self.ffn_dropout_p = ffn_dropout_p - self.attn_dropout_p = attn_dropout_p - self.resid_dropout_p = resid_dropout_p - - self.s1_bits = s1_bits - self.s2_bits = s2_bits - self.codebook_dim = s1_bits + s2_bits # Total dimension of the codebook after quantization - self.embed = nn.Linear(self.d_in, self.d_model) - self.head = nn.Linear(self.d_model, self.d_in) - - # Encoder Transformer Blocks - self.encoder = nn.ModuleList([ - TransformerBlock(self.d_model, self.n_heads, self.ff_dim, self.ffn_dropout_p, self.attn_dropout_p, self.resid_dropout_p) - for _ in range(self.enc_layers - 1) - ]) - # Decoder Transformer Blocks - self.decoder = nn.ModuleList([ - TransformerBlock(self.d_model, self.n_heads, self.ff_dim, self.ffn_dropout_p, self.attn_dropout_p, self.resid_dropout_p) - for _ in range(self.dec_layers - 1) - ]) - self.quant_embed = nn.Linear(in_features=self.d_model, out_features=self.codebook_dim) # Linear layer before quantization - self.post_quant_embed_pre = nn.Linear(in_features=self.s1_bits, out_features=self.d_model) # Linear layer after quantization (pre part - s1 bits) - self.post_quant_embed = nn.Linear(in_features=self.codebook_dim, out_features=self.d_model) # Linear layer after quantization (full codebook) - self.tokenizer = BSQuantizer(self.s1_bits, self.s2_bits, beta, gamma0, gamma, zeta, group_size) # BSQuantizer module - - def forward(self, x): - """ - Forward pass of the KronosTokenizer. - - Args: - x (torch.Tensor): Input tensor of shape (batch_size, seq_len, d_in). - - Returns: - tuple: A tuple containing: - - tuple: (z_pre, z) - Reconstructed outputs from decoder with s1_bits and full codebook respectively, - both of shape (batch_size, seq_len, d_in). - - torch.Tensor: bsq_loss - Loss from the BSQuantizer. - - torch.Tensor: quantized - Quantized representation from BSQuantizer. - - torch.Tensor: z_indices - Indices from the BSQuantizer. - """ - z = self.embed(x) - - for layer in self.encoder: - z = layer(z) - - z = self.quant_embed(z) # (B, T, codebook) - - bsq_loss, quantized, z_indices = self.tokenizer(z) - - quantized_pre = quantized[:, :, :self.s1_bits] # Extract the first part of quantized representation (s1_bits) - z_pre = self.post_quant_embed_pre(quantized_pre) - - z = self.post_quant_embed(quantized) - - # Decoder layers (for pre part - s1 bits) - for layer in self.decoder: - z_pre = layer(z_pre) - z_pre = self.head(z_pre) - - # Decoder layers (for full codebook) - for layer in self.decoder: - z = layer(z) - z = self.head(z) - - return (z_pre, z), bsq_loss, quantized, z_indices - - def indices_to_bits(self, x, half=False): - """ - Converts indices to bit representations and scales them. - - Args: - x (torch.Tensor): Indices tensor. - half (bool, optional): Whether to process only half of the codebook dimension. Defaults to False. - - Returns: - torch.Tensor: Bit representation tensor. - """ - if half: - x1 = x[0] # Assuming x is a tuple of indices if half is True - x2 = x[1] - mask = 2 ** torch.arange(self.codebook_dim//2, device=x1.device, dtype=torch.long) # Create a mask for bit extraction - x1 = (x1.unsqueeze(-1) & mask) != 0 # Extract bits for the first half - x2 = (x2.unsqueeze(-1) & mask) != 0 # Extract bits for the second half - x = torch.cat([x1, x2], dim=-1) # Concatenate the bit representations - else: - mask = 2 ** torch.arange(self.codebook_dim, device=x.device, dtype=torch.long) # Create a mask for bit extraction - x = (x.unsqueeze(-1) & mask) != 0 # Extract bits - - x = x.float() * 2 - 1 # Convert boolean to bipolar (-1, 1) - q_scale = 1. / (self.codebook_dim ** 0.5) # Scaling factor - x = x * q_scale - return x - - def encode(self, x, half=False): - """ - Encodes the input data into quantized indices. - - Args: - x (torch.Tensor): Input tensor of shape (batch_size, seq_len, d_in). - half (bool, optional): Whether to use half quantization in BSQuantizer. Defaults to False. - - Returns: - torch.Tensor: Quantized indices from BSQuantizer. - """ - z = self.embed(x) - for layer in self.encoder: - z = layer(z) - z = self.quant_embed(z) - - bsq_loss, quantized, z_indices = self.tokenizer(z, half=half, collect_metrics=False) - return z_indices - - def decode(self, x, half=False): - """ - Decodes quantized indices back to the input data space. - - Args: - x (torch.Tensor): Quantized indices tensor. - half (bool, optional): Whether the indices were generated with half quantization. Defaults to False. - - Returns: - torch.Tensor: Reconstructed output tensor of shape (batch_size, seq_len, d_in). - """ - quantized = self.indices_to_bits(x, half) - z = self.post_quant_embed(quantized) - for layer in self.decoder: - z = layer(z) - z = self.head(z) - return z - - -class Kronos(nn.Module, PyTorchModelHubMixin): - """ - Kronos Model. - - Args: - s1_bits (int): Number of bits for pre tokens. - s2_bits (int): Number of bits for post tokens. - n_layers (int): Number of Transformer blocks. - d_model (int): Dimension of the model's embeddings and hidden states. - n_heads (int): Number of attention heads in the MultiheadAttention layers. - ff_dim (int): Dimension of the feedforward network in the Transformer blocks. - ffn_dropout_p (float): Dropout probability for the feedforward network. - attn_dropout_p (float): Dropout probability for the attention layers. - resid_dropout_p (float): Dropout probability for residual connections. - token_dropout_p (float): Dropout probability for token embeddings. - learn_te (bool): Whether to use learnable temporal embeddings. - """ - - def __init__(self, s1_bits, s2_bits, n_layers, d_model, n_heads, ff_dim, ffn_dropout_p, attn_dropout_p, resid_dropout_p, token_dropout_p, learn_te, news_dim=None): - super().__init__() - self.s1_bits = s1_bits - self.s2_bits = s2_bits - self.n_layers = n_layers - self.d_model = d_model - self.n_heads = n_heads - self.learn_te = learn_te - self.ff_dim = ff_dim - self.ffn_dropout_p = ffn_dropout_p - self.attn_dropout_p = attn_dropout_p - self.resid_dropout_p = resid_dropout_p - self.token_dropout_p = token_dropout_p - self.news_dim = news_dim - - self.s1_vocab_size = 2 ** self.s1_bits - self.token_drop = nn.Dropout(self.token_dropout_p) - self.embedding = HierarchicalEmbedding(self.s1_bits, self.s2_bits, self.d_model) - self.time_emb = TemporalEmbedding(self.d_model, self.learn_te) - self.transformer = nn.ModuleList([ - TransformerBlock(self.d_model, self.n_heads, self.ff_dim, self.ffn_dropout_p, self.attn_dropout_p, self.resid_dropout_p) - for _ in range(self.n_layers) - ]) - self.norm = RMSNorm(self.d_model) - self.dep_layer = DependencyAwareLayer(self.d_model) - self.head = DualHead(self.s1_bits, self.s2_bits, self.d_model) - - if self.news_dim is not None: - self.news_proj = nn.Linear(self.news_dim, self.d_model) - else: - self.news_proj = None - - self.apply(self._init_weights) - - def _init_weights(self, module): - - if isinstance(module, nn.Linear): - nn.init.xavier_normal_(module.weight) - if module.bias is not None: - nn.init.zeros_(module.bias) - elif isinstance(module, nn.Embedding): - nn.init.normal_(module.weight, mean=0, std=self.embedding.d_model ** -0.5) - elif isinstance(module, nn.LayerNorm): - nn.init.ones_(module.weight) - nn.init.zeros_(module.bias) - elif isinstance(module, RMSNorm): - nn.init.ones_(module.weight) - - def forward(self, s1_ids, s2_ids, stamp=None, padding_mask=None, use_teacher_forcing=False, s1_targets=None, news_emb=None): - """ - Args: - s1_ids (torch.Tensor): Input tensor of s1 token IDs. Shape: [batch_size, seq_len] - s2_ids (torch.Tensor): Input tensor of s2 token IDs. Shape: [batch_size, seq_len] - stamp (torch.Tensor, optional): Temporal stamp tensor. Shape: [batch_size, seq_len]. Defaults to None. - padding_mask (torch.Tensor, optional): Mask for padding tokens. Shape: [batch_size, seq_len]. Defaults to None. - use_teacher_forcing (bool, optional): Whether to use teacher forcing for s1 decoding. Defaults to False. - s1_targets (torch.Tensor, optional): Target s1 token IDs for teacher forcing. Shape: [batch_size, seq_len]. Defaults to None. - news_emb (torch.Tensor, optional): News embedding tensor. Shape: [batch_size, news_dim]. Defaults to None. - - Returns: - Tuple[torch.Tensor, torch.Tensor]: - - s1 logits: Logits for s1 token predictions. Shape: [batch_size, seq_len, s1_vocab_size] - - s2_logits: Logits for s2 token predictions, conditioned on s1. Shape: [batch_size, seq_len, s2_vocab_size] - """ - x = self.embedding([s1_ids, s2_ids]) - if stamp is not None: - time_embedding = self.time_emb(stamp) - x = x + time_embedding - x = self.token_drop(x) - - for layer in self.transformer: - x = layer(x, key_padding_mask=padding_mask) - - x = self.norm(x) - - if news_emb is not None and self.news_proj is not None: - news_bias = self.news_proj(news_emb).unsqueeze(1) # [B, 1, d_model] - x = x + news_bias - - s1_logits = self.head(x) - - if use_teacher_forcing: - sibling_embed = self.embedding.emb_s1(s1_targets) - else: - s1_probs = F.softmax(s1_logits.detach(), dim=-1) - sample_s1_ids = torch.multinomial(s1_probs.view(-1, self.s1_vocab_size), 1).view(s1_ids.shape) - sibling_embed = self.embedding.emb_s1(sample_s1_ids) - - x2 = self.dep_layer(x, sibling_embed, key_padding_mask=padding_mask) # Dependency Aware Layer: Condition on s1 embeddings - s2_logits = self.head.cond_forward(x2) - return s1_logits, s2_logits - - def decode_s1(self, s1_ids, s2_ids, stamp=None, padding_mask=None, news_emb=None): - """ - Decodes only the s1 tokens. - - This method performs a forward pass to predict only s1 tokens. It returns the s1 logits - and the context representation from the Transformer, which can be used for subsequent s2 decoding. - - Args: - s1_ids (torch.Tensor): Input tensor of s1 token IDs. Shape: [batch_size, seq_len] - s2_ids (torch.Tensor): Input tensor of s2 token IDs. Shape: [batch_size, seq_len] - stamp (torch.Tensor, optional): Temporal stamp tensor. Shape: [batch_size, seq_len]. Defaults to None. - padding_mask (torch.Tensor, optional): Mask for padding tokens. Shape: [batch_size, seq_len]. Defaults to None. - news_emb (torch.Tensor, optional): News embedding tensor. Shape: [batch_size, news_dim]. Defaults to None. - - Returns: - Tuple[torch.Tensor, torch.Tensor]: - - s1 logits: Logits for s1 token predictions. Shape: [batch_size, seq_len, s1_vocab_size] - - context: Context representation from the Transformer. Shape: [batch_size, seq_len, d_model] - """ - x = self.embedding([s1_ids, s2_ids]) - if stamp is not None: - time_embedding = self.time_emb(stamp) - x = x + time_embedding - x = self.token_drop(x) - - for layer in self.transformer: - x = layer(x, key_padding_mask=padding_mask) - - x = self.norm(x) - - if news_emb is not None and self.news_proj is not None: - news_bias = self.news_proj(news_emb).unsqueeze(1) # [B, 1, d_model] - x = x + news_bias - - s1_logits = self.head(x) - return s1_logits, x - - def decode_s2(self, context, s1_ids, padding_mask=None): - """ - Decodes the s2 tokens, conditioned on the context and s1 tokens. - - This method decodes s2 tokens based on a pre-computed context representation (typically from `decode_s1`) - and the s1 token IDs. It uses the dependency-aware layer and the conditional s2 head to predict s2 tokens. - - Args: - context (torch.Tensor): Context representation from the transformer (output of decode_s1). - Shape: [batch_size, seq_len, d_model] - s1_ids (torch.torch.Tensor): Input tensor of s1 token IDs. Shape: [batch_size, seq_len] - padding_mask (torch.Tensor, optional): Mask for padding tokens. Shape: [batch_size, seq_len]. Defaults to None. - - Returns: - torch.Tensor: s2 logits. Shape: [batch_size, seq_len, s2_vocab_size] - """ - sibling_embed = self.embedding.emb_s1(s1_ids) - x2 = self.dep_layer(context, sibling_embed, key_padding_mask=padding_mask) - return self.head.cond_forward(x2) - - -def top_k_top_p_filtering( - logits, - top_k: int = 0, - top_p: float = 1.0, - filter_value: float = -float("Inf"), - min_tokens_to_keep: int = 1, -): - """Filter a distribution of logits using top-k and/or nucleus (top-p) filtering - Args: - logits: logits distribution shape (batch size, vocabulary size) - if top_k > 0: keep only top k tokens with highest probability (top-k filtering). - if top_p < 1.0: keep the top tokens with cumulative probability >= top_p (nucleus filtering). - Nucleus filtering is described in Holtzman et al. (http://arxiv.org/abs/1904.09751) - Make sure we keep at least min_tokens_to_keep per batch example in the output - From: https://gist.github.com/thomwolf/1a5a29f6962089e871b94cbd09daf317 - """ - if top_k > 0: - top_k = min(max(top_k, min_tokens_to_keep), logits.size(-1)) # Safety check - # Remove all tokens with a probability less than the last token of the top-k - indices_to_remove = logits < torch.topk(logits, top_k)[0][..., -1, None] - logits[indices_to_remove] = filter_value - return logits - - if top_p < 1.0: - sorted_logits, sorted_indices = torch.sort(logits, descending=True) - cumulative_probs = torch.cumsum(F.softmax(sorted_logits, dim=-1), dim=-1) - - # Remove tokens with cumulative probability above the threshold (token with 0 are kept) - sorted_indices_to_remove = cumulative_probs > top_p - if min_tokens_to_keep > 1: - # Keep at least min_tokens_to_keep (set to min_tokens_to_keep-1 because we add the first one below) - sorted_indices_to_remove[..., :min_tokens_to_keep] = 0 - # Shift the indices to the right to keep also the first token above the threshold - sorted_indices_to_remove[..., 1:] = sorted_indices_to_remove[..., :-1].clone() - sorted_indices_to_remove[..., 0] = 0 - - # scatter sorted tensors to original indexing - indices_to_remove = sorted_indices_to_remove.scatter(1, sorted_indices, sorted_indices_to_remove) - logits[indices_to_remove] = filter_value - return logits - - -def sample_from_logits(logits, temperature=1.0, top_k=None, top_p=None, sample_logits=True): - logits = logits / temperature - if top_k is not None or top_p is not None: - if top_k > 0 or top_p < 1.0: - logits = top_k_top_p_filtering(logits, top_k=top_k, top_p=top_p) - - probs = F.softmax(logits, dim=-1) - - if not sample_logits: - _, x = top_k(probs, k=1, dim=-1) - else: - x = torch.multinomial(probs, num_samples=1) - - return x - - -def auto_regressive_inference(tokenizer, model, x, x_stamp, y_stamp, max_context, pred_len, clip=5, T=1.0, top_k=0, top_p=0.99, sample_count=5, verbose=False, news_emb=None): - with torch.no_grad(): - x = torch.clip(x, -clip, clip) - - device = x.device - x = x.unsqueeze(1).repeat(1, sample_count, 1, 1).reshape(-1, x.size(1), x.size(2)).to(device) - x_stamp = x_stamp.unsqueeze(1).repeat(1, sample_count, 1, 1).reshape(-1, x_stamp.size(1), x_stamp.size(2)).to(device) - y_stamp = y_stamp.unsqueeze(1).repeat(1, sample_count, 1, 1).reshape(-1, y_stamp.size(1), y_stamp.size(2)).to(device) - - x_token = tokenizer.encode(x, half=True) - - initial_seq_len = x.size(1) - batch_size = x_token[0].size(0) - total_seq_len = initial_seq_len + pred_len - full_stamp = torch.cat([x_stamp, y_stamp], dim=1) - - generated_pre = x_token[0].new_empty(batch_size, pred_len) - generated_post = x_token[1].new_empty(batch_size, pred_len) - - pre_buffer = x_token[0].new_zeros(batch_size, max_context) - post_buffer = x_token[1].new_zeros(batch_size, max_context) - buffer_len = min(initial_seq_len, max_context) - if buffer_len > 0: - start_idx = max(0, initial_seq_len - max_context) - pre_buffer[:, :buffer_len] = x_token[0][:, start_idx:start_idx + buffer_len] - post_buffer[:, :buffer_len] = x_token[1][:, start_idx:start_idx + buffer_len] - - if verbose: - ran = trange - else: - ran = range - for i in ran(pred_len): - current_seq_len = initial_seq_len + i - window_len = min(current_seq_len, max_context) - - if current_seq_len <= max_context: - input_tokens = [ - pre_buffer[:, :window_len], - post_buffer[:, :window_len] - ] - else: - input_tokens = [pre_buffer, post_buffer] - - context_end = current_seq_len - context_start = max(0, context_end - max_context) - current_stamp = full_stamp[:, context_start:context_end, :].contiguous() - - s1_logits, context = model.decode_s1(input_tokens[0], input_tokens[1], current_stamp, news_emb=news_emb) - s1_logits = s1_logits[:, -1, :] - sample_pre = sample_from_logits(s1_logits, temperature=T, top_k=top_k, top_p=top_p, sample_logits=True) - - s2_logits = model.decode_s2(context, sample_pre) - s2_logits = s2_logits[:, -1, :] - sample_post = sample_from_logits(s2_logits, temperature=T, top_k=top_k, top_p=top_p, sample_logits=True) - - generated_pre[:, i] = sample_pre.squeeze(-1) - generated_post[:, i] = sample_post.squeeze(-1) - - if current_seq_len < max_context: - pre_buffer[:, current_seq_len] = sample_pre.squeeze(-1) - post_buffer[:, current_seq_len] = sample_post.squeeze(-1) - else: - pre_buffer.copy_(torch.roll(pre_buffer, shifts=-1, dims=1)) - post_buffer.copy_(torch.roll(post_buffer, shifts=-1, dims=1)) - pre_buffer[:, -1] = sample_pre.squeeze(-1) - post_buffer[:, -1] = sample_post.squeeze(-1) - - full_pre = torch.cat([x_token[0], generated_pre], dim=1) - full_post = torch.cat([x_token[1], generated_post], dim=1) - - context_start = max(0, total_seq_len - max_context) - input_tokens = [ - full_pre[:, context_start:total_seq_len].contiguous(), - full_post[:, context_start:total_seq_len].contiguous() - ] - z = tokenizer.decode(input_tokens, half=True) - z = z.reshape(-1, sample_count, z.size(1), z.size(2)) - preds = z.cpu().numpy() - preds = np.mean(preds, axis=1) - - return preds - - -def calc_time_stamps(x_timestamp): - time_df = pd.DataFrame() - time_df['minute'] = x_timestamp.dt.minute - time_df['hour'] = x_timestamp.dt.hour - time_df['weekday'] = x_timestamp.dt.weekday - time_df['day'] = x_timestamp.dt.day - time_df['month'] = x_timestamp.dt.month - return time_df - - -class KronosPredictor: - - def __init__(self, model, tokenizer, device="cuda:0", max_context=512, clip=5): - self.tokenizer = tokenizer - self.model = model - self.max_context = max_context - self.clip = clip - self.price_cols = ['open', 'high', 'low', 'close'] - self.vol_col = 'volume' - self.amt_vol = 'amount' - self.time_cols = ['minute', 'hour', 'weekday', 'day', 'month'] - self.device = device - - self.tokenizer = self.tokenizer.to(self.device) - self.model = self.model.to(self.device) - - def generate(self, x, x_stamp, y_stamp, pred_len, T, top_k, top_p, sample_count, verbose, news_emb=None): - - x_tensor = torch.from_numpy(np.array(x).astype(np.float32)).to(self.device) - x_stamp_tensor = torch.from_numpy(np.array(x_stamp).astype(np.float32)).to(self.device) - y_stamp_tensor = torch.from_numpy(np.array(y_stamp).astype(np.float32)).to(self.device) - - preds = auto_regressive_inference(self.tokenizer, self.model, x_tensor, x_stamp_tensor, y_stamp_tensor, self.max_context, pred_len, - self.clip, T, top_k, top_p, sample_count, verbose, news_emb=news_emb) - preds = preds[:, -pred_len:, :] - return preds - - def predict(self, df, x_timestamp, y_timestamp, pred_len, T=1.0, top_k=0, top_p=0.9, sample_count=1, verbose=True, news_emb=None): - - if not isinstance(df, pd.DataFrame): - raise ValueError("Input must be a pandas DataFrame.") - - if not all(col in df.columns for col in self.price_cols): - raise ValueError(f"Price columns {self.price_cols} not found in DataFrame.") - - df = df.copy() - if self.vol_col not in df.columns: - df[self.vol_col] = 0.0 # Fill missing volume with zeros - df[self.amt_vol] = 0.0 # Fill missing amount with zeros - if self.amt_vol not in df.columns and self.vol_col in df.columns: - df[self.amt_vol] = df[self.vol_col] * df[self.price_cols].mean(axis=1) - - if df[self.price_cols + [self.vol_col, self.amt_vol]].isnull().values.any(): - raise ValueError("Input DataFrame contains NaN values in price or volume columns.") - - x_time_df = calc_time_stamps(x_timestamp) - y_time_df = calc_time_stamps(y_timestamp) - - x = df[self.price_cols + [self.vol_col, self.amt_vol]].values.astype(np.float32) - x_stamp = x_time_df.values.astype(np.float32) - y_stamp = y_time_df.values.astype(np.float32) - - x_mean, x_std = np.mean(x, axis=0), np.std(x, axis=0) - - x = (x - x_mean) / (x_std + 1e-5) - x = np.clip(x, -self.clip, self.clip) - - x = x[np.newaxis, :] - x_stamp = x_stamp[np.newaxis, :] - y_stamp = y_stamp[np.newaxis, :] - - if news_emb is not None: - news_emb_tensor = torch.from_numpy(np.array(news_emb).astype(np.float32)).to(self.device) - # Ensure batch dimension for news_emb if only one sample - if news_emb_tensor.ndim == 1: - news_emb_tensor = news_emb_tensor.unsqueeze(0) - else: - news_emb_tensor = None - - preds = self.generate(x, x_stamp, y_stamp, pred_len, T, top_k, top_p, sample_count, verbose, news_emb=news_emb_tensor) - - preds = preds.squeeze(0) - preds = preds * (x_std + 1e-5) + x_mean - - pred_df = pd.DataFrame(preds, columns=self.price_cols + [self.vol_col, self.amt_vol], index=y_timestamp) - return pred_df - - - def predict_batch(self, df_list, x_timestamp_list, y_timestamp_list, pred_len, T=1.0, top_k=0, top_p=0.9, sample_count=1, verbose=True): - """ - Perform parallel (batch) prediction on multiple time series. All series must have the same historical length and prediction length (pred_len). - - Args: - df_list (List[pd.DataFrame]): List of input DataFrames, each containing price columns and optional volume/amount columns. - x_timestamp_list (List[pd.DatetimeIndex or Series]): List of timestamps corresponding to historical data, length should match the number of rows in each DataFrame. - y_timestamp_list (List[pd.DatetimeIndex or Series]): List of future prediction timestamps, length should equal pred_len. - pred_len (int): Number of prediction steps. - T (float): Sampling temperature. - top_k (int): Top-k filtering threshold. - top_p (float): Top-p (nucleus sampling) threshold. - sample_count (int): Number of parallel samples per series, automatically averaged internally. - verbose (bool): Whether to display autoregressive progress. - - Returns: - List[pd.DataFrame]: List of prediction results in the same order as input, each DataFrame contains - `open, high, low, close, volume, amount` columns, indexed by corresponding `y_timestamp`. - """ - # Basic validation - if not isinstance(df_list, (list, tuple)) or not isinstance(x_timestamp_list, (list, tuple)) or not isinstance(y_timestamp_list, (list, tuple)): - raise ValueError("df_list, x_timestamp_list, y_timestamp_list must be list or tuple types.") - if not (len(df_list) == len(x_timestamp_list) == len(y_timestamp_list)): - raise ValueError("df_list, x_timestamp_list, y_timestamp_list must have consistent lengths.") - - num_series = len(df_list) - - x_list = [] - x_stamp_list = [] - y_stamp_list = [] - means = [] - stds = [] - seq_lens = [] - y_lens = [] - - for i in range(num_series): - df = df_list[i] - if not isinstance(df, pd.DataFrame): - raise ValueError(f"Input at index {i} is not a pandas DataFrame.") - if not all(col in df.columns for col in self.price_cols): - raise ValueError(f"DataFrame at index {i} is missing price columns {self.price_cols}.") - - df = df.copy() - if self.vol_col not in df.columns: - df[self.vol_col] = 0.0 - df[self.amt_vol] = 0.0 - if self.amt_vol not in df.columns and self.vol_col in df.columns: - df[self.amt_vol] = df[self.vol_col] * df[self.price_cols].mean(axis=1) - - if df[self.price_cols + [self.vol_col, self.amt_vol]].isnull().values.any(): - raise ValueError(f"DataFrame at index {i} contains NaN values in price or volume columns.") - - x_timestamp = x_timestamp_list[i] - y_timestamp = y_timestamp_list[i] - - x_time_df = calc_time_stamps(x_timestamp) - y_time_df = calc_time_stamps(y_timestamp) - - x = df[self.price_cols + [self.vol_col, self.amt_vol]].values.astype(np.float32) - x_stamp = x_time_df.values.astype(np.float32) - y_stamp = y_time_df.values.astype(np.float32) - - if x.shape[0] != x_stamp.shape[0]: - raise ValueError(f"Inconsistent lengths at index {i}: x has {x.shape[0]} vs x_stamp has {x_stamp.shape[0]}.") - if y_stamp.shape[0] != pred_len: - raise ValueError(f"y_timestamp length at index {i} should equal pred_len={pred_len}, got {y_stamp.shape[0]}.") - - x_mean, x_std = np.mean(x, axis=0), np.std(x, axis=0) - x_norm = (x - x_mean) / (x_std + 1e-5) - x_norm = np.clip(x_norm, -self.clip, self.clip) - - x_list.append(x_norm) - x_stamp_list.append(x_stamp) - y_stamp_list.append(y_stamp) - means.append(x_mean) - stds.append(x_std) - - seq_lens.append(x_norm.shape[0]) - y_lens.append(y_stamp.shape[0]) - - # Require all series to have consistent historical and prediction lengths for batch processing - if len(set(seq_lens)) != 1: - raise ValueError(f"Parallel prediction requires all series to have consistent historical lengths, got: {seq_lens}") - if len(set(y_lens)) != 1: - raise ValueError(f"Parallel prediction requires all series to have consistent prediction lengths, got: {y_lens}") - - x_batch = np.stack(x_list, axis=0).astype(np.float32) # (B, seq_len, feat) - x_stamp_batch = np.stack(x_stamp_list, axis=0).astype(np.float32) # (B, seq_len, time_feat) - y_stamp_batch = np.stack(y_stamp_list, axis=0).astype(np.float32) # (B, pred_len, time_feat) - - preds = self.generate(x_batch, x_stamp_batch, y_stamp_batch, pred_len, T, top_k, top_p, sample_count, verbose) - # preds: (B, pred_len, feat) - - pred_dfs = [] - for i in range(num_series): - preds_i = preds[i] * (stds[i] + 1e-5) + means[i] - pred_df = pd.DataFrame(preds_i, columns=self.price_cols + [self.vol_col, self.amt_vol], index=y_timestamp_list[i]) - pred_dfs.append(pred_df) - - return pred_dfs diff --git a/skills/alphaear-predictor/scripts/utils/predictor/model/module.py b/skills/alphaear-predictor/scripts/utils/predictor/model/module.py deleted file mode 100644 index 20b29b5..0000000 --- a/skills/alphaear-predictor/scripts/utils/predictor/model/module.py +++ /dev/null @@ -1,562 +0,0 @@ -import math - -from einops import rearrange, reduce -import torch -import torch.nn as nn -from torch.autograd import Function -import torch.nn.functional as F - - -class DifferentiableEntropyFunction(Function): - @staticmethod - def forward(ctx, zq, basis, K, eps): - zb = (zq + 1) / 2 - zi = ((zb * basis).sum(-1)).to(torch.int64) - cnt = torch.scatter_reduce(torch.zeros(2 ** K, device=zq.device, dtype=zq.dtype), - 0, - zi.flatten(), - torch.ones_like(zi.flatten()).to(zq.dtype), - 'sum') - prob = (cnt + eps) / (cnt + eps).sum() - H = -(prob * torch.log(prob)).sum() - ctx.save_for_backward(zq, zi, prob) - ctx.K = K - return H - - @staticmethod - def backward(ctx, grad_output): - zq, zi, prob = ctx.saved_tensors - grad_array = -grad_output * (torch.log(prob) + 1) / zi.numel() / ctx.K - reord_grad = grad_array[zi.flatten()].reshape(zi.shape) - grad_input = reord_grad.unsqueeze(-1) * zq - return grad_input, None, None, None, None - - -def codebook_entropy(zq, basis, K, eps=1e-4): - return DifferentiableEntropyFunction.apply(zq, basis, K, eps) - - -class BinarySphericalQuantizer(nn.Module): - def __init__(self, embed_dim, beta, gamma0, gamma, zeta, - input_format='bchw', - soft_entropy=True, group_size=9, - persample_entropy_compute='analytical', - cb_entropy_compute='group', - l2_norm=True, - inv_temperature=1): - """ - Paper link: https://arxiv.org/pdf/2406.07548.pdf - Here we use the official implementation of the BinarySphericalQuantizer. - """ - super().__init__() - self.embed_dim = embed_dim - self.beta = beta # loss weight for commit loss - self.gamma0 = gamma0 # loss weight for entropy penalty - self.gamma = gamma # loss weight for entropy penalty - self.zeta = zeta # loss weight for entire entropy penalty - self.input_format = input_format - assert self.embed_dim % group_size == 0, "embed_dim must be divisible by group_size" - self.num_groups = self.embed_dim // group_size - self.group_size = group_size - assert persample_entropy_compute in ['group', 'analytical'], "persample_entropy_compute must be either 'group' or 'analytical'" - assert cb_entropy_compute in ['group', 'nce'], "cb_entropy_compute must be either 'group' or 'nce'" - self.persample_entropy_compute = persample_entropy_compute - self.cb_entropy_compute = cb_entropy_compute - self.l2_norm = l2_norm - self.inv_temperature = inv_temperature - - self.register_buffer('basis', 2 ** torch.arange(embed_dim - 1, -1, -1)) - self.register_buffer('group_basis', 2 ** torch.arange(group_size - 1, -1, -1)) - - self.num_dimensions = 2 ** embed_dim - self.bits_per_index = embed_dim - - # we only need to keep the codebook portion up to the group size - # because we approximate the H loss with this subcode - group_codes = torch.arange(2 ** self.group_size) - group_codebook = self.indexes_to_codes(group_codes).float()[:, -group_size:] - self.register_buffer('group_codebook', group_codebook, persistent=False) - - self.soft_entropy = soft_entropy # soft_entropy: Sec 3.2 of https://arxiv.org/pdf/1911.05894.pdf - - def quantize(self, z): - assert z.shape[-1] == self.embed_dim, f"Expected {self.embed_dim} dimensions, got {z.shape[-1]}" - - zhat = torch.where(z > 0, - torch.tensor(1, dtype=z.dtype, device=z.device), - torch.tensor(-1, dtype=z.dtype, device=z.device)) - return z + (zhat - z).detach() - - def forward(self, z, collect_metrics=True): - # if self.input_format == 'bchw': - # z = rearrange(z, 'b c h w -> b h w c') - zq = self.quantize(z) - - q_scale = 1. / (self.embed_dim ** 0.5) if self.l2_norm else 1. - - zq = zq * q_scale - - if not collect_metrics: - return zq, zq.new_zeros(()), {} - - indices = self.codes_to_indexes(zq.detach()) - group_indices = self.codes_to_group_indexes(zq.detach()) - if not self.training: - used_codes = torch.unique(indices, return_counts=False) - else: - used_codes = None - - if self.soft_entropy: - persample_entropy, cb_entropy, avg_prob = self.soft_entropy_loss(z) - entropy_penalty = self.gamma0 * persample_entropy - self.gamma * cb_entropy - else: - zb_by_sample = ((zq + 1) / 2).reshape(z.shape[0], -1, z.shape[-1]).to(torch.float32) - persample_entropy = self.get_hard_per_sample_entropy(zb_by_sample) - cb_entropy = codebook_entropy(zq, self.basis, self.embed_dim) - entropy_penalty = self.gamma0 * persample_entropy - self.gamma * cb_entropy - - # commit loss - commit_loss = self.beta * torch.mean(((zq.detach() - z) ** 2).sum(dim=-1)) - - # if self.input_format == 'bchw': - # zq = rearrange(zq, 'b h w c -> b c h w') - - return ( - zq, - commit_loss + self.zeta * entropy_penalty / self.inv_temperature, - {"H": cb_entropy, "used_codes": used_codes, "indices": indices, "group_indices": group_indices, - "avg_prob": avg_prob} - ) - - def soft_entropy_loss(self, z): - # if we divide the code in subgroups of size group_size, the codebook will be of size 2 ** group_size - # the sub-code is the last group_size bits of the full code - group_code_book = self.group_codebook / (self.embed_dim ** 0.5 if self.l2_norm else 1) - divided_z = rearrange(z, '... (g c) -> ... g c', c=self.group_size) - - # we calculate the distance between the divided_z and the codebook for each subgroup - distance = - 2 * torch.einsum('... g c, d c ->... g d', divided_z, group_code_book) - prob = (-distance * self.inv_temperature).softmax(dim=-1) - if self.persample_entropy_compute == 'analytical': - if self.l2_norm: - p = torch.sigmoid(-4 * z / (self.embed_dim ** 0.5) * self.inv_temperature) - else: - p = torch.sigmoid(-4 * z * self.inv_temperature) - prob = torch.stack([p, 1 - p], dim=-1) - per_sample_entropy = self.get_entropy(prob, dim=-1, normalize=False).sum(dim=-1).mean() - else: - per_sample_entropy = self.get_entropy(prob, dim=-1, normalize=False).sum(dim=-1).mean() - - # macro average of the probability of each subgroup - avg_prob = reduce(prob, '... g d ->g d', 'mean') - codebook_entropy = self.get_entropy(avg_prob, dim=-1, normalize=False) - - # the approximation of the entropy is the sum of the entropy of each subgroup - return per_sample_entropy, codebook_entropy.sum(), avg_prob - - def get_hard_per_sample_entropy(self, zb_by_sample): - probs_per_dim = zb_by_sample.sum(1) / zb_by_sample.shape[1] - persample_entropy = - probs_per_dim * torch.log(probs_per_dim + 1e-8) - (1 - probs_per_dim) * torch.log(1 - probs_per_dim + 1e-8) - persample_entropy = persample_entropy.sum(-1) - return persample_entropy.mean() - - def codes_to_indexes(self, zhat): - """Converts a `code` to an index in the codebook. - Args: - zhat: A tensor of shape (B, ..., C) containing the codes. must be in {-1, 1} - """ - assert zhat.shape[-1] == self.embed_dim, f"Expected {self.embed_dim} dimensions, got {zhat.shape[-1]}" - return ((zhat + 1) / 2 * self.basis).sum(axis=-1).to(torch.int64) - - def codes_to_group_indexes(self, zhat): - """Converts a `code` to a list of indexes (in groups) in the codebook. - Args: - zhat: A tensor of shape (B, ..., C) containing the codes. must be in {-1, 1} - """ - zhat_in_group = rearrange(zhat, 'b ... (g c) -> b ... g c', c=self.group_size) - return ((zhat_in_group + 1) / 2 * self.group_basis).sum(axis=-1).to(torch.int64) - - def indexes_to_codes(self, indices): - """Inverse of `indexes_to_codes`.""" - indices = indices.unsqueeze(-1) - codes_non_centered = torch.remainder( - torch.floor_divide(indices, self.basis), 2 - ) - return codes_non_centered * 2 - 1 - - def group_indexes_to_codes(self, group_indices): - """Inverse of `group_indexes_to_codes`.""" - group_indices = group_indices.unsqueeze(-1) - codes_non_centered = torch.remainder( - torch.floor_divide(group_indices, self.group_basis), 2 - ) - codes_non_centered = rearrange(codes_non_centered, 'b ... g c -> b ... (g c)') - return codes_non_centered * 2 - 1 - - def get_entropy(self, count, dim=-1, eps=1e-4, normalize=True): - if normalize: - probs = (count + eps) / (count + eps).sum(dim=dim, keepdim=True) - else: - probs = count - H = -(probs * torch.log(probs + 1e-8)).sum(dim=dim) - return H - - def get_group_codebook_entry(self, group_indices): - z_q = self.group_indexes_to_codes(group_indices) - q_scale = 1. / (self.embed_dim ** 0.5) if self.l2_norm else 1. - z_q = z_q * q_scale - if self.input_format == 'bchw': - h, w = int(z_q.shape[1] ** 0.5) - assert h * w == z_q.shape[1], 'Invalid sequence length' - z_q = rearrange(z_q, 'b (h w) c -> b c h w', h=h) - return z_q - - def get_codebook_entry(self, indices): - z_q = self.indexes_to_codes(indices) - q_scale = 1. / (self.embed_dim ** 0.5) if self.l2_norm else 1. - z_q = z_q * q_scale - if self.input_format == 'bchw': - h, w = int(z_q.shape[1] ** 0.5) - assert h * w == z_q.shape[1], 'Invalid sequence length' - z_q = rearrange(z_q, 'b (h w) c -> b c h w', h=h) - return z_q - - -class BSQuantizer(nn.Module): - - def __init__(self, s1_bits, s2_bits, beta, gamma0, gamma, zeta, group_size): - super().__init__() - self.codebook_dim = s1_bits + s2_bits - self.s1_bits = s1_bits - self.s2_bits = s2_bits - self.bsq = BinarySphericalQuantizer(self.codebook_dim, beta, gamma0, gamma, zeta, group_size=group_size) - - def bits_to_indices(self, bits): - bits = (bits >= 0).to(torch.long) - indices = 2 ** torch.arange( - 0, - bits.shape[-1], - 1, - dtype=torch.long, - device=bits.device, - ) - return (bits * indices).sum(-1) - - def forward(self, z, half=False, collect_metrics=True): - z = F.normalize(z, dim=-1) - quantized, bsq_loss, metrics = self.bsq(z, collect_metrics=collect_metrics) - if half: - q_pre = quantized[:, :, :self.s1_bits] - q_post = quantized[:, :, self.s1_bits:] - z_indices = [self.bits_to_indices(q_pre), self.bits_to_indices(q_post)] - else: - z_indices = self.bits_to_indices(quantized) - return bsq_loss, quantized, z_indices - - -class RMSNorm(torch.nn.Module): - def __init__(self, dim: int, eps: float = 1e-5): - super().__init__() - self.eps = eps - self.weight = nn.Parameter(torch.ones(dim)) - - def _norm(self, x): - return x * torch.rsqrt(torch.mean(x * x, dim=-1, keepdim=True) + self.eps) - - def forward(self, x): - output = self._norm(x.float()).type_as(x) - return output * self.weight - - -class FeedForward(nn.Module): - def __init__(self, d_model, ff_dim, ffn_dropout_p=0.0): - super().__init__() - - self.w1 = nn.Linear(d_model, ff_dim, bias=False) - self.w3 = nn.Linear(d_model, ff_dim, bias=False) - self.w2 = nn.Linear(ff_dim, d_model, bias=False) - self.ffn_dropout = nn.Dropout(ffn_dropout_p) - - def forward(self, x): - return self.ffn_dropout(self.w2(F.silu(self.w1(x)) * self.w3(x))) - - -class RotaryPositionalEmbedding(nn.Module): - def __init__(self, dim): - super().__init__() - inv_freq = 1.0 / (10000 ** (torch.arange(0, dim, 2).float() / dim)) - self.register_buffer("inv_freq", inv_freq) - self.seq_len_cached = None - self.cos_cached = None - self.sin_cached = None - - def _update_cos_sin_cache(self, x, seq_len): - if seq_len != self.seq_len_cached: - self.seq_len_cached = seq_len - t = torch.arange(seq_len, device=x.device).type_as(self.inv_freq) - freqs = torch.einsum('i,j->ij', t, self.inv_freq) - emb = torch.cat((freqs, freqs), dim=-1).to(x.device) - self.cos_cached = emb.cos()[None, None, :, :] - self.sin_cached = emb.sin()[None, None, :, :] - return self.cos_cached, self.sin_cached - - def forward(self, q, k): - cos, sin = self._update_cos_sin_cache(q, q.shape[-2]) - return ( - (q * cos) + (self._rotate_half(q) * sin), - (k * cos) + (self._rotate_half(k) * sin), - ) - - def _rotate_half(self, x): - x1, x2 = x.chunk(2, dim=-1) - return torch.cat((-x2, x1), dim=-1) - - -class MultiHeadAttentionWithRoPE(nn.Module): - def __init__(self, d_model, n_heads, attn_dropout_p=0.0, resid_dropout_p=0.0): - super().__init__() - self.d_model = d_model - self.n_heads = n_heads - self.head_dim = d_model // n_heads - - self.q_proj = nn.Linear(d_model, d_model) - self.k_proj = nn.Linear(d_model, d_model) - self.v_proj = nn.Linear(d_model, d_model) - self.out_proj = nn.Linear(d_model, d_model) - self.rotary = RotaryPositionalEmbedding(self.head_dim) - self.attn_dropout_p = attn_dropout_p - self.resid_dropout = nn.Dropout(resid_dropout_p) - - def forward(self, x, key_padding_mask=None): - batch_size, seq_len, _ = x.shape - - q = self.q_proj(x).view(batch_size, seq_len, self.n_heads, self.head_dim).transpose(1, 2) - k = self.k_proj(x).view(batch_size, seq_len, self.n_heads, self.head_dim).transpose(1, 2) - v = self.v_proj(x).view(batch_size, seq_len, self.n_heads, self.head_dim).transpose(1, 2) - - q, k = self.rotary(q, k) - - if key_padding_mask is not None: - attn_mask = key_padding_mask.unsqueeze(1).unsqueeze(2) # [batch, 1, 1, seq_len] - attn_mask = attn_mask.expand(-1, self.n_heads, seq_len, -1) # [batch, n_heads, q_len, k_len] - else: - attn_mask = None - - attn_output = F.scaled_dot_product_attention( - q, k, v, - attn_mask=attn_mask, - dropout_p=self.attn_dropout_p if self.training else 0.0, - is_causal=True - ) - - attn_output = attn_output.transpose(1, 2).contiguous().view(batch_size, seq_len, self.d_model) - return self.resid_dropout(self.out_proj(attn_output)) - - -class MultiHeadCrossAttentionWithRoPE(nn.Module): - def __init__(self, d_model, n_heads, attn_dropout_p=0.0, resid_dropout=0.0): - super().__init__() - self.d_model = d_model - self.n_heads = n_heads - self.head_dim = d_model // n_heads - - self.q_proj = nn.Linear(d_model, d_model) - self.k_proj = nn.Linear(d_model, d_model) - self.v_proj = nn.Linear(d_model, d_model) - self.out_proj = nn.Linear(d_model, d_model) - self.rotary = RotaryPositionalEmbedding(self.head_dim) - self.attn_dropout_p = attn_dropout_p - self.resid_dropout = nn.Dropout(resid_dropout) - - def forward(self, query, key, value, key_padding_mask=None): - batch_size, q_len, _ = query.shape - _, seq_len, _ = key.shape - - q = self.q_proj(query).view(batch_size, q_len, self.n_heads, self.head_dim).transpose(1, 2) - k = self.k_proj(key).view(batch_size, seq_len, self.n_heads, self.head_dim).transpose(1, 2) - v = self.v_proj(value).view(batch_size, seq_len, self.n_heads, self.head_dim).transpose(1, 2) - - q, k = self.rotary(q, k) - - if key_padding_mask is not None: - attn_mask = key_padding_mask.unsqueeze(1).unsqueeze(2) - attn_mask = attn_mask.expand(-1, self.n_heads, q_len, -1) - else: - attn_mask = None - - is_causal_flag = self.training - - attn_output = F.scaled_dot_product_attention( - q, k, v, - attn_mask=attn_mask, - dropout_p=self.attn_dropout_p if self.training else 0.0, - is_causal=is_causal_flag - ) - - attn_output = attn_output.transpose(1, 2).contiguous().view(batch_size, q_len, self.d_model) - return self.resid_dropout(self.out_proj(attn_output)) - - -class HierarchicalEmbedding(nn.Module): - def __init__(self, s1_bits, s2_bits, d_model=256): - super().__init__() - self.s1_bits = s1_bits - self.s2_bits = s2_bits - - vocab_s1 = 2 ** s1_bits - vocab_s2 = 2 ** s2_bits - - self.emb_s1 = nn.Embedding(vocab_s1, d_model) - self.emb_s2 = nn.Embedding(vocab_s2, d_model) - self.d_model = d_model - self.fusion_proj = nn.Linear(d_model * 2, d_model) - - nn.init.normal_(self.emb_s1.weight, mean=0, std=d_model ** -0.5) - nn.init.normal_(self.emb_s2.weight, mean=0, std=d_model ** -0.5) - - def split_token(self, token_ids: torch.Tensor, s2_bits: int): - """Inputs: - token_ids (torch.Tensor): Composite token IDs of shape [batch_size, seq_len] or [N], each in range [0, 2^(s1_bits + s2_bits) - 1]. - s2_bits (int): Number of low bits used for the fine token (s2). - """ - assert isinstance(s2_bits, int) and s2_bits > 0, "s2_bits must be a positive integer" - - t = token_ids.long() - mask = (1 << s2_bits) - 1 - s2_ids = t & mask # extract low bits - s1_ids = t >> s2_bits # extract high bits - return s1_ids, s2_ids - - def forward(self, token_ids): - """Inputs: - token_ids: - - tuple or list: (s1_ids, s2_ids), each of shape [batch_size, seq_len], or - - torch.Tensor: composite token IDs of shape [batch_size, seq_len], which will be split into (s1_ids, s2_ids) internally. - Output: [batch_size, seq_len, d_model] - """ - if isinstance(token_ids, tuple) or isinstance(token_ids, list): - s1_ids, s2_ids = token_ids - else: - s1_ids, s2_ids = self.split_token(token_ids, self.s2_bits) - s1_emb = self.emb_s1(s1_ids) * math.sqrt(self.d_model) - s2_emb = self.emb_s2(s2_ids) * math.sqrt(self.d_model) - return self.fusion_proj(torch.cat([s1_emb, s2_emb], dim=-1)) - - -class DependencyAwareLayer(nn.Module): - def __init__(self, d_model, n_heads=4, attn_dropout_p=0.0, resid_dropout=0.0): - super().__init__() - self.cross_attn = MultiHeadCrossAttentionWithRoPE(d_model, n_heads, attn_dropout_p, resid_dropout) - self.norm = RMSNorm(d_model) - - def forward(self, hidden_states, sibling_embed, key_padding_mask=None): - """hidden_states: [batch, seq_len, d_model] - sibling_embed: Embedding from another subtoken - """ - attn_out = self.cross_attn( - query=sibling_embed, - key=hidden_states, - value=hidden_states, - key_padding_mask=key_padding_mask - ) - return self.norm(hidden_states + attn_out) - - -class TransformerBlock(nn.Module): - def __init__(self, d_model, n_heads, ff_dim=1024, ffn_dropout_p=0.0, attn_dropout_p=0.0, resid_dropout_p=0.0): - super().__init__() - self.norm1 = RMSNorm(d_model) - self.self_attn = MultiHeadAttentionWithRoPE(d_model, n_heads, attn_dropout_p, resid_dropout_p) - self.norm2 = RMSNorm(d_model) - self.ffn = FeedForward(d_model, ff_dim, ffn_dropout_p) - - def forward(self, x, key_padding_mask=None): - residual = x - x = self.norm1(x) - attn_out = self.self_attn(x, key_padding_mask=key_padding_mask) - x = residual + attn_out - - residual = x - x = self.norm2(x) - ffn_out = self.ffn(x) - x = residual + ffn_out - return x - - -class DualHead(nn.Module): - def __init__(self, s1_bits, s2_bits, d_model): - super().__init__() - self.vocab_s1 = 2 ** s1_bits - self.vocab_s2 = 2 ** s2_bits - self.proj_s1 = nn.Linear(d_model, self.vocab_s1) - self.proj_s2 = nn.Linear(d_model, self.vocab_s2) - - def compute_loss(self, s1_logits, s2_logits, s1_targets, s2_targets, padding_mask=None): - if padding_mask is not None: - valid_mask = (padding_mask == 0) - s1_logits = s1_logits[valid_mask] - s2_logits = s2_logits[valid_mask] - s1_targets = s1_targets[valid_mask] - s2_targets = s2_targets[valid_mask] - ce_s1 = F.cross_entropy(s1_logits, s1_targets) - ce_s2 = F.cross_entropy(s2_logits, s2_targets) - else: - ce_s1 = F.cross_entropy(s1_logits.reshape(-1, self.vocab_s1), s1_targets.reshape(-1)) - ce_s2 = F.cross_entropy(s2_logits.reshape(-1, self.vocab_s2), s2_targets.reshape(-1)) - ce_loss = (ce_s1 + ce_s2) / 2 - return ce_loss, ce_s1, ce_s2 - - def forward(self, x): - return self.proj_s1(x) - - def cond_forward(self, x2): - return self.proj_s2(x2) - - -class FixedEmbedding(nn.Module): - def __init__(self, c_in, d_model): - super(FixedEmbedding, self).__init__() - - w = torch.zeros(c_in, d_model).float() - w.require_grad = False - - position = torch.arange(0, c_in).float().unsqueeze(1) - div_term = (torch.arange(0, d_model, 2).float() * -(math.log(10000.0) / d_model)).exp() - - w[:, 0::2] = torch.sin(position * div_term) - w[:, 1::2] = torch.cos(position * div_term) - - self.emb = nn.Embedding(c_in, d_model) - self.emb.weight = nn.Parameter(w, requires_grad=False) - - def forward(self, x): - return self.emb(x).detach() - - -class TemporalEmbedding(nn.Module): - def __init__(self, d_model, learn_pe): - super(TemporalEmbedding, self).__init__() - - minute_size = 60 - hour_size = 24 - weekday_size = 7 - day_size = 32 - month_size = 13 - - Embed = FixedEmbedding if not learn_pe else nn.Embedding - self.minute_embed = Embed(minute_size, d_model) - self.hour_embed = Embed(hour_size, d_model) - self.weekday_embed = Embed(weekday_size, d_model) - self.day_embed = Embed(day_size, d_model) - self.month_embed = Embed(month_size, d_model) - - def forward(self, x): - x = x.long() - - minute_x = self.minute_embed(x[:, :, 0]) - hour_x = self.hour_embed(x[:, :, 1]) - weekday_x = self.weekday_embed(x[:, :, 2]) - day_x = self.day_embed(x[:, :, 3]) - month_x = self.month_embed(x[:, :, 4]) - - return hour_x + weekday_x + day_x + month_x + minute_x \ No newline at end of file diff --git a/skills/alphaear-predictor/scripts/utils/predictor/training.py b/skills/alphaear-predictor/scripts/utils/predictor/training.py deleted file mode 100644 index 3b41724..0000000 --- a/skills/alphaear-predictor/scripts/utils/predictor/training.py +++ /dev/null @@ -1,539 +0,0 @@ -import os -import sys -import time -import torch -import torch.nn as nn -import pandas as pd -import numpy as np -import json -import random -from loguru import logger -from datetime import datetime, timedelta -from sentence_transformers import SentenceTransformer -from skills._env_loader import load_unified_env - -load_unified_env() - -# Setup paths -KRONOS_DIR = os.path.dirname(os.path.abspath(__file__)) -SRC_DIR = os.path.dirname(os.path.dirname(KRONOS_DIR)) -if SRC_DIR not in sys.path: - sys.path.insert(0, SRC_DIR) - -from ..kronos.model import Kronos, KronosTokenizer, KronosPredictor -from ..database_manager import DatabaseManager -from ..stock_tools import StockTools -from ..search_tools import SearchTools -from ..llm.factory import get_model -from ..visualizer import VisualizerTools -from ..schema.models import ForecastResult, KLinePoint -from agno.agent import Agent - - -class AutoSynthesisTrainer: - def __init__(self, news_dim=384): - self.device = ( - "cuda" - if torch.cuda.is_available() - else "mps" - if torch.backends.mps.is_available() - else "cpu" - ) - self.db = DatabaseManager() - self.tools = StockTools(self.db) - self.searcher = SearchTools(self.db) - # Try loading from local cache first to avoid network timeouts - model_name = os.getenv( - "EMBEDDING_MODEL", "sentence-transformers/all-MiniLM-L6-v2" - ) - try: - logger.info(f"🔄 Attempting to load {model_name} from local cache...") - self.embedder = SentenceTransformer( - model_name, device=self.device, local_files_only=True - ) - logger.success("✅ Model loaded from local cache.") - except Exception: - logger.warning( - "⚠️ Local cache not found or incomplete. Attempting to download..." - ) - self.embedder = SentenceTransformer(model_name, device=self.device) - self.news_dim = news_dim - - # Try loading from local cache first to avoid network timeouts - try: - logger.info( - "🔄 Attempting to load Kronos and Tokenizer from local cache..." - ) - self.tokenizer = KronosTokenizer.from_pretrained( - "NeoQuasar/Kronos-Tokenizer-base", local_files_only=True - ).to(self.device) - base_model = Kronos.from_pretrained( - "NeoQuasar/Kronos-base", local_files_only=True - ) - logger.success("✅ Kronos and Tokenizer loaded from local cache.") - except Exception: - logger.warning( - "⚠️ Local Kronos/Tokenizer not found or incomplete. Attempting to download..." - ) - self.tokenizer = KronosTokenizer.from_pretrained( - "NeoQuasar/Kronos-Tokenizer-base" - ).to(self.device) - base_model = Kronos.from_pretrained("NeoQuasar/Kronos-base") - - self.model = Kronos( - base_model.s1_bits, - base_model.s2_bits, - base_model.n_layers, - base_model.d_model, - base_model.n_heads, - base_model.ff_dim, - base_model.ffn_dropout_p, - base_model.attn_dropout_p, - base_model.resid_dropout_p, - base_model.token_dropout_p, - base_model.learn_te, - news_dim=self.news_dim, - ).to(self.device) - self.model.load_state_dict(base_model.state_dict(), strict=False) - - # LLM for causality verification - provider = os.getenv("LLM_PROVIDER", "minimax") - model_id = os.getenv("LLM_MODEL", "Qwen") - self.llm_agent = Agent(model=get_model(provider, model_id)) - - def discover_shocks( - self, ticker_list, threshold=2.0, limit_per_stock=5, days=365, pred_len=5 - ): - """1. Find days with significant price movements (Look back 1 year)""" - shocks = [] - end_date = datetime.now().strftime("%Y-%m-%d") - start_date = (datetime.now() - timedelta(days=days)).strftime("%Y-%m-%d") - - for ticker in ticker_list: - df = self.tools.get_stock_price( - ticker, start_date=start_date, end_date=end_date - ) - if df.empty or len(df) < 60: - continue - - # Look for big moves - moves = df[df["change_pct"].abs() > threshold].copy() - if moves.empty: - continue - - count = 0 - for idx, row in moves.iterrows(): - # Ensure we have history before this day AND enough future days for eval - date_idx = df.index.get_loc(idx) - if date_idx < 50 or date_idx + pred_len > len(df): - continue - - shocks.append( - { - "ticker": ticker, - "date": row["date"], - "change": row["change_pct"], - "history": df.iloc[date_idx - 50 : date_idx], - "target": df.iloc[ - date_idx : date_idx + pred_len - ], # Now capturing pred_len days - } - ) - count += 1 - if count >= limit_per_stock: - break - - logger.info( - f"✨ Discovered {len(shocks)} potential price shocks over the last {days} days." - ) - return shocks - - def find_reason_and_verify(self, shock): - """2. Search for reasons and verify causality using LLM""" - ticker_info = self.db.get_stock_by_code(shock["ticker"]) - name = ticker_info["name"] if ticker_info else shock["ticker"] - date_str = shock["date"] - - # Try multiple query variations and engines - queries = [ - f"{name} ({shock['ticker']}) {date_str} 为什么涨跌 原因", - f"{name} {date_str} 异动 原因", - f"{shock['ticker']} {date_str} 新闻", - ] - - search_results = [] - for query in queries: - logger.info(f"🔍 Searching for reason: {query}") - # Try alternate engines - for engine in ["baidu"]: - try: - results = self.searcher.search_list( - query, engine=engine, max_results=3, enrich=False - ) - if results: - search_results = results - break - except Exception as e: - logger.warning(f"Search failed for {query} on {engine}: {e}") - - if search_results: - break - time.sleep(random.uniform(1.0, 2.0)) - - if not search_results: - logger.warning( - f"⚠️ No search results found for {name} on {date_str} after multiple attempts." - ) - return None - - context = "\n".join( - [f"- {r['title']}: {r.get('content', '')[:300]}" for r in search_results] - ) - - prompt = f""" - 任务:判断以下新闻是否解释了该股票在 {date_str} 的 {shock["change"]:.2f}% 价格变动。 - - 股票:{name} - 日期:{date_str} - 变动:{shock["change"]:.2f}% - - 搜索结果: - {context} - - 要求: - 1. 该新闻是否在该日期左右发生? - 2. 该新闻是否能逻辑上解释这种大幅波动(如财报、利好政策、重组、大环境暴跌等)? - 3. 如果是,请总结一段 100 字以内的“核心推动原因”。 - 4. 返回 JSON: {{"is_causal": true/false, "summary": "原因摘要"}} - """ - - try: - res = self.llm_agent.run(prompt) - data = json.loads( - res.content.replace("```json", "").replace("```", "").strip() - ) - if data.get("is_causal"): - logger.success( - f"✅ Verified cause for {name} on {date_str}: {data['summary']}" - ) - return data["summary"] - else: - logger.warning( - f"❌ Verified cause for {name} on {date_str}: {data['summary']}" - ) - return None - except Exception as e: - logger.warning(f"Verification failed: {e}") - return None - - def save_model(self, path=None): - """Save the news_proj weights""" - if path is None: - save_dir = os.path.join(SRC_DIR, "exports/models") - os.makedirs(save_dir, exist_ok=True) - path = os.path.join( - save_dir, f"kronos_news_v1_{datetime.now().strftime('%Y%m%d_%H%M')}.pt" - ) - - # We only really need to save the news_proj part as it's the only one we train - torch.save( - { - "news_proj_state_dict": self.model.news_proj.state_dict(), - "news_dim": self.news_dim, - "d_model": self.model.d_model, - }, - path, - ) - logger.success(f"💾 Model weights saved to {path}") - return path - - def run_synthesis_and_train(self, tickers, pred_len=5): - # 1. Discovery - shocks = self.discover_shocks(tickers, pred_len=pred_len) - print(f"find {len(shocks)} shocks") - - # 2. News Association & Verification - dataset = [] - max_news_items = 200 # Limit to 200 news items per session to avoid search bans - - logger.info( - f"🧬 Starting News Association for {len(shocks)} shocks (Max limit: {max_news_items})" - ) - - for i, shock in enumerate(shocks): - if len(dataset) >= max_news_items: - logger.info("Reached maximum news items limit for this session.") - break - - summary = self.find_reason_and_verify(shock) - if summary: - # 3. Embedding news - emb = self.embedder.encode(summary) - dataset.append( - { - "history": shock["history"], - "target": shock["target"], - "news_emb": emb, - "summary": summary, - } - ) - - # Add delay after search with randomness to avoid being blocked - if i < len(shocks) - 1: - delay = random.uniform(2.0, 4.0) - time.sleep(delay) - - if not dataset: - logger.error( - "❌ No verified news-price pairs found. Adjust threshold or check if news is available in that period." - ) - return - - # 4. Train/Val Split - random.seed(42) - random.shuffle(dataset) - - if len(dataset) < 2: - train_set = dataset - val_set = [] - logger.warning( - f"⚠️ Only {len(dataset)} sample(s) found. Training on all, skipping validation." - ) - else: - split_idx = max(1, int(len(dataset) * 0.8)) - if split_idx >= len(dataset): - split_idx = len(dataset) - 1 - - train_set = dataset[:split_idx] - val_set = dataset[split_idx:] - logger.info( - f"🏗️ Dataset Split: {len(train_set)} samples for training, {len(val_set)} for validation." - ) - - if not train_set: - logger.error("❌ No samples for training.") - return - - # 5. Training (Few-shot) - optimizer = torch.optim.Adam(self.model.news_proj.parameters(), lr=1e-3) - criterion = nn.CrossEntropyLoss() - self.model.train() - - loss_history = [] - logger.info(f"🚀 Training for 30 epochs...") - for epoch in range(30): - total_loss = 0 - for item in train_set: - optimizer.zero_grad() - - # Prep Data - hist_df = item["history"] - # For training, we still focus on the immediate next point (teacher forcing) - target_df = item["target"].iloc[:1] - - hist_raw = hist_df[ - ["open", "high", "low", "close", "volume"] - ].values.astype(np.float32) - hist_raw = np.column_stack([hist_raw, hist_raw[:, 3] * hist_raw[:, 4]]) - - mean, std = hist_raw.mean(axis=0), hist_raw.std(axis=0) + 1e-5 - hist_norm = ( - torch.from_numpy((hist_raw - mean) / std) - .unsqueeze(0) - .to(self.device) - ) - - target_raw = target_df[ - ["open", "high", "low", "close", "volume"] - ].values.astype(np.float32) - target_raw = np.column_stack( - [target_raw, target_raw[:, 3] * target_raw[:, 4]] - ) - target_norm = ( - torch.from_numpy((target_raw - mean) / std) - .unsqueeze(0) - .to(self.device) - ) - - with torch.no_grad(): - z_indices = self.tokenizer.encode(hist_norm, half=True) - t_indices = self.tokenizer.encode(target_norm, half=True) - s1_ids, s2_ids = z_indices[0], z_indices[1] - t_s1, t_s2 = t_indices[0], t_indices[1] - - news_t = torch.from_numpy(item["news_emb"]).unsqueeze(0).to(self.device) - s1_logits, s2_logits = self.model( - s1_ids, - s2_ids, - news_emb=news_t, - use_teacher_forcing=True, - s1_targets=t_s1, - ) - - loss = ( - criterion(s1_logits[:, -1, :], t_s1[:, 0]) - + criterion(s2_logits[:, -1, :], t_s2[:, 0]) - ) / 2 - loss.backward() - optimizer.step() - total_loss += loss.item() - - avg_epoch_loss = total_loss / max(1, len(train_set)) - loss_history.append(avg_epoch_loss) - - if (epoch + 1) % 10 == 0: - logger.info(f"Epoch {epoch + 1} Loss: {avg_epoch_loss:.4f}") - - # 5.1 Visualize Loss Curve - loss_chart = VisualizerTools.generate_loss_chart(loss_history) - VisualizerTools.render_chart_to_file( - loss_chart, - os.path.join(SRC_DIR, "exports/training_results/loss_curve.html"), - ) - - # 5.2 Save final model - self.save_model() - - # 6. Final Evaluation on Validation Set - if not val_set: - logger.warning("⚠️ Validation set is empty. Skipping statistical analysis.") - return - - logger.info( - f"🧪 Final Evaluation: Base vs News-Integrated ({pred_len}-day Window)" - ) - self.model.eval() - predictor = KronosPredictor(self.model, self.tokenizer, device=self.device) - - base_maes = [] - news_maes = [] - - print("\n" + "=" * 90) - print( - f"{'Date':<12} | {'Ticker':<8} | {'Base MAE':<15} | {'News MAE':<15} | {'Improvement'}" - ) - print("-" * 90) - - for item in val_set: - h = item["history"] - t = item["target"] - actuals = t["close"].values[:pred_len] - - x_ts = pd.to_datetime(h["date"]) - # Future timestamps: handle business days if possible, or just simple offset - future_dates = pd.date_range( - start=x_ts.iloc[-1] + timedelta(days=1), periods=pred_len, freq="B" - ) - y_ts = pd.Series(future_dates) - - # A. Base Prediction - p_base = predictor.predict( - h, x_ts, y_ts, pred_len=pred_len, news_emb=None, verbose=False - ) - b_preds = p_base["close"].values[: len(actuals)] - - # B. News-Aware Prediction - p_news = predictor.predict( - h, - x_ts, - y_ts, - pred_len=pred_len, - news_emb=item["news_emb"], - verbose=False, - ) - n_preds = p_news["close"].values[: len(actuals)] - - # Calculate MAE over the window - b_mae = np.mean(np.abs(b_preds - actuals)) - n_mae = np.mean(np.abs(n_preds - actuals)) - - base_maes.append(b_mae) - news_maes.append(n_mae) - - improvement = (b_mae - n_mae) / (b_mae + 1e-6) * 100 - - date_str = str(t["date"].values[0])[:10] - ticker = h.iloc[-1]["ticker"] if "ticker" in h.columns else "Stock" - print( - f"{date_str:<12} | {ticker:<8} | {b_mae:<15.4f} | {n_mae:<15.4f} | {improvement:>+7.1f}%" - ) - - # C. Generate Visualization for this case - try: - # Helper to convert DF to KLinePoints - def to_kp_list(preds_df): - points = [] - for idx, row in preds_df.iterrows(): - points.append( - KLinePoint( - date=str(idx)[:10], - open=row["open"], - high=row["high"], - low=row["low"], - close=row["close"], - volume=row["volume"] if "volume" in row else 0, - ) - ) - return points - - forecast_obj = ForecastResult( - ticker=ticker, - base_forecast=to_kp_list(p_base), - adjusted_forecast=to_kp_list(p_news), - rationale=item["summary"], - ) - - # Ground truth for visualizer expects a DataFrame with 'date' and 'close' - gt_df = t[["date", "open", "high", "low", "close", "volume"]] - - chart = VisualizerTools.generate_stock_chart( - df=h, - ticker=ticker, - title=f"Training Eval: {ticker} ({date_str}) Improvement: {improvement:.1f}%", - forecast=forecast_obj, - ground_truth=gt_df, - ) - - safe_date = date_str.replace("-", "") - filename = f"eval_{ticker}_{safe_date}.html" - VisualizerTools.render_chart_to_file( - chart, os.path.join(SRC_DIR, f"exports/training_results/{filename}") - ) - except Exception as e: - logger.error(f"Failed to generate eval chart for {ticker}: {e}") - - # Summary Statistics - avg_base_err = sum(base_maes) / max(1, len(base_maes)) - avg_news_err = sum(news_maes) / max(1, len(news_maes)) - overall_imp = (avg_base_err - avg_news_err) / (avg_base_err + 1e-6) * 100 - - print("-" * 90) - print( - f"{'AVERAGE':<12} | {'-':<8} | {avg_base_err:<15.4f} | {avg_news_err:<15.4f} | {overall_imp:>+7.1f}%" - ) - print("=" * 90 + "\n") - - logger.success( - f"🏁 Statistical Analysis Complete. Avg Error Reduction ({pred_len}-day): {overall_imp:.2f}%" - ) - logger.info( - f"📊 Visualization results saved to: {os.path.join(SRC_DIR, 'exports/training_results/')}" - ) - - -if __name__ == "__main__": - trainer = AutoSynthesisTrainer() - - logger.info("📂 Fetching all stock codes from database...") - res = trainer.db.execute_query("SELECT code FROM stock_list") - all_tickers = [row["code"] for row in res] - - if not all_tickers: - logger.warning("⚠️ No tickers found in stock_list table. Trying to sync...") - trainer.tools._check_and_update_stock_list(force=True) - res = trainer.db.execute_query("SELECT code FROM stock_list") - all_tickers = [row["code"] for row in res] - - logger.info(f"🚀 Starting training on potential stocks (1-year scan)...") - # 为了演示,我们扫描前 100 个股票,寻找最近一年的冲击点 - trainer.run_synthesis_and_train(all_tickers[:100], pred_len=1) diff --git a/skills/alphaear-predictor/scripts/utils/search_tools.py b/skills/alphaear-predictor/scripts/utils/search_tools.py deleted file mode 100644 index 50b08f3..0000000 --- a/skills/alphaear-predictor/scripts/utils/search_tools.py +++ /dev/null @@ -1,611 +0,0 @@ -import os -import hashlib -import json -import re -import requests -import time -import threading -from typing import List, Dict, Optional, Any -from agno.tools.duckduckgo import DuckDuckGoTools -from agno.tools.baidusearch import BaiduSearchTools -from agno.agent import Agent -from loguru import logger -from datetime import datetime -from .database_manager import DatabaseManager -from .content_extractor import ContentExtractor -from .llm.factory import get_model -from .hybrid_search import LocalNewsSearch - -# 默认搜索缓存 TTL(秒),可通过环境变量覆盖 -DEFAULT_SEARCH_TTL = int(os.getenv("SEARCH_CACHE_TTL", "3600")) # 默认 1 小时 - - -class JinaSearchEngine: - """Jina Search API 封装 - 使用 s.jina.ai 进行网络搜索""" - - JINA_SEARCH_URL = "https://s.jina.ai/" - - # 速率限制配置 - _rate_limit_no_key = 10 # 无 key 时每分钟最大请求数 - _rate_window = 60.0 - _min_interval = 2.0 - _request_times = [] - _last_request_time = 0.0 - _lock = threading.Lock() - - def __init__(self): - self.api_key = os.getenv("JINA_API_KEY", "").strip() - self.has_api_key = bool(self.api_key) - if self.has_api_key: - logger.info("✅ Jina Search API key configured") - - @classmethod - def _wait_for_rate_limit(cls, has_api_key: bool) -> None: - """等待以满足速率限制""" - if has_api_key: - time.sleep(0.3) - return - - with cls._lock: - current_time = time.time() - cls._request_times = [t for t in cls._request_times if current_time - t < cls._rate_window] - - if len(cls._request_times) >= cls._rate_limit_no_key: - oldest = cls._request_times[0] - wait_time = cls._rate_window - (current_time - oldest) + 1.0 - if wait_time > 0: - logger.warning(f"⏳ Jina Search rate limit, waiting {wait_time:.1f}s...") - time.sleep(wait_time) - current_time = time.time() - cls._request_times = [t for t in cls._request_times if current_time - t < cls._rate_window] - - time_since_last = current_time - cls._last_request_time - if time_since_last < cls._min_interval: - time.sleep(cls._min_interval - time_since_last) - - cls._request_times.append(time.time()) - cls._last_request_time = time.time() - - def search(self, query: str, max_results: int = 5) -> List[Dict]: - """ - 使用 Jina Search API 执行搜索 - - Args: - query: 搜索关键词 - max_results: 返回结果数量 - - Returns: - 搜索结果列表,每个结果包含 title, url, content - """ - if not query: - return [] - - logger.info(f"🔍 Jina Search: {query}") - - # 等待速率限制 - self._wait_for_rate_limit(self.has_api_key) - - headers = { - "Accept": "application/json", - "X-Retain-Images": "none", - } - - if self.has_api_key: - headers["Authorization"] = f"Bearer {self.api_key}" - - try: - # Jina Search API: https://s.jina.ai/{query} - import urllib.parse - encoded_query = urllib.parse.quote(query) - url = f"{self.JINA_SEARCH_URL}{encoded_query}" - - response = requests.get(url, headers=headers, timeout=30) - - if response.status_code == 429: - logger.warning("⚠️ Jina Search rate limited (429), waiting 30s...") - time.sleep(30) - return self.search(query, max_results) - - if response.status_code != 200: - logger.warning(f"Jina Search failed (Status {response.status_code})") - return [] - - # 解析响应 - try: - data = response.json() - except json.JSONDecodeError: - # 如果返回纯文本,尝试解析 - data = {"data": [{"title": "Search Result", "url": "", "content": response.text}]} - - results = [] - - # Jina 返回格式可能是 {"data": [...]} 或直接是列表 - items = data.get("data", []) if isinstance(data, dict) else data - if not isinstance(items, list): - items = [items] if items else [] - - for i, item in enumerate(items[:max_results]): - if isinstance(item, dict): - results.append({ - "title": item.get("title", f"Result {i+1}"), - "url": item.get("url", ""), - "href": item.get("url", ""), # 兼容性 - "content": item.get("content", item.get("description", "")), - "body": item.get("content", item.get("description", "")), # 兼容性 - }) - elif isinstance(item, str): - results.append({ - "title": f"Result {i+1}", - "url": "", - "content": item - }) - - logger.info(f"✅ Jina Search returned {len(results)} results") - return results - - except requests.exceptions.Timeout: - logger.error("Jina Search timeout") - return [] - except requests.exceptions.RequestException as e: - logger.error(f"Jina Search request error: {e}") - return [] - except Exception as e: - logger.error(f"Jina Search unexpected error: {e}") - return [] - -class SearchTools: - """扩展性搜索工具库 - 支持多引擎聚合与内容缓存""" - - def __init__(self, db: DatabaseManager): - self.db = db - - # 检查 Jina API Key 是否配置 - jina_api_key = os.getenv("JINA_API_KEY", "").strip() - self._jina_enabled = bool(jina_api_key) - - self._engines = { - "ddg": DuckDuckGoTools(), - "baidu": BaiduSearchTools(), - "local": LocalNewsSearch(db) - } - - # 如果配置了 Jina API Key,添加 Jina 引擎 - if self._jina_enabled: - self._engines["jina"] = JinaSearchEngine() - logger.info("🚀 Jina Search engine enabled (JINA_API_KEY configured)") - - # 确定默认搜索引擎 - self._default_engine = "jina" if self._jina_enabled else "ddg" - - def _generate_hash(self, query: str, engine: str, max_results: int) -> str: - return hashlib.md5(f"{engine}:{query}:{max_results}".encode()).hexdigest() - - def search(self, query: str, engine: str = None, max_results: int = 5, ttl: Optional[int] = None) -> str: - """ - 使用指定搜索引擎执行网络搜索,结果会被缓存以提高效率。 - - Args: - query: 搜索关键词,如 "英伟达财报" 或 "光伏行业政策"。 - engine: 搜索引擎选择。可选值: - "jina" (Jina Search,需配置 JINA_API_KEY,LLM友好输出), - "ddg" (DuckDuckGo,推荐英文/国际搜索), - "baidu" (百度,推荐中文/国内搜索), - "local" (本地历史新闻搜索,基于向量+BM25)。 - 默认: 若配置了 JINA_API_KEY 则使用 "jina",否则 "ddg"。 - max_results: 期望返回的结果数量,默认 5 条。 - ttl: 缓存有效期(秒)。如果缓存超过此时间会重新搜索。 - 默认使用环境变量 SEARCH_CACHE_TTL 或 3600 秒。 - 设为 0 可强制刷新。 - - Returns: - 搜索结果的文本描述,包含标题、摘要和链接。 - """ - # 使用默认引擎(如果配置了 Jina 则优先使用 Jina) - if engine is None: - engine = self._default_engine - - if engine not in self._engines: - return f"Error: Unsupported engine '{engine}'. Available: {list(self._engines.keys())}" - - query_hash = self._generate_hash(query, engine, max_results) - effective_ttl = ttl if ttl is not None else DEFAULT_SEARCH_TTL - - # 1. 尝试从缓存读取 (local 引擎不缓存,因为它本身就是查库) - if engine != "local": - cache = self.db.get_search_cache(query_hash, ttl_seconds=effective_ttl if effective_ttl > 0 else None) - if cache and effective_ttl != 0: - logger.info(f"ℹ️ Found search results in cache for: {query} ({engine})") - return cache['results'] - - # 2. 执行真实搜索 - logger.info(f"📡 Searching {engine} for: {query}") - try: - tool = self._engines[engine] - if engine == "jina": - # Jina Search 返回 List[Dict] - jina_results = tool.search(query, max_results=max_results) - results = [] - for r in jina_results: - results.append({ - "title": r.get("title", ""), - "href": r.get("url", ""), - "body": r.get("content", "") - }) - elif engine == "ddg": - results = tool.duckduckgo_search(query, max_results=max_results) - elif engine == "baidu": - results = tool.baidu_search(query, max_results=max_results) - elif engine == "local": - # LocalNewsSearch 返回的是 List[Dict] - local_results = tool.search(query, top_n=max_results) - results = [] - for r in local_results: - results.append({ - "title": r.get("title"), - "href": r.get("url", "local"), - "body": r.get("content", "") - }) - else: - results = "Search not implemented for this engine." - - results_str = str(results) - if engine != "local": - self.db.save_search_cache(query_hash, query, engine, results_str) - return results_str - - except Exception as e: - # 搜索失败时的降级策略 - if engine == "jina": - logger.warning(f"⚠️ Jina search failed, falling back to ddg: {query} ({e})") - try: - return self.search(query, engine="ddg", max_results=max_results, ttl=ttl) - except Exception as e2: - logger.error(f"❌ DDG fallback also failed for {query}: {e2}") - elif engine == "ddg": - logger.warning(f"⚠️ DDG search failed, falling back to baidu: {query} ({e})") - try: - return self.search(query, engine="baidu", max_results=max_results, ttl=ttl) - except Exception as e2: - logger.error(f"❌ Baidu fallback also failed for {query}: {e2}") - - logger.error(f"❌ Search failed for {query}: {e}") - return f"Error occurred during search: {str(e)}" - - def search_list(self, query: str, engine: str = None, max_results: int = 5, ttl: Optional[int] = None, enrich: bool = True) -> List[Dict]: - """ - 执行搜索并返回结构化列表 (List[Dict])。 - Dict 包含: title, href (or url), body (or snippet) - - Args: - engine: 搜索引擎,默认使用配置的默认引擎(Jina 优先) - enrich: 是否抓取正文内容 (默认 True) - """ - # 使用默认引擎 - if engine is None: - engine = self._default_engine - - if engine not in self._engines: - logger.error(f"Unsupported engine {engine}") - return [] - - # 不同的 hash 以区分是否 enrichment - enrich_suffix = ":enriched" if enrich else "" - query_hash = self._generate_hash(query, engine + enrich_suffix, max_results) - effective_ttl = ttl if ttl is not None else DEFAULT_SEARCH_TTL - - # 1. 尝试从缓存读取 - cache = self.db.get_search_cache(query_hash, ttl_seconds=effective_ttl if effective_ttl > 0 else None) - if cache and effective_ttl != 0: - try: - cached_data = json.loads(cache['results']) - if isinstance(cached_data, list): - logger.info(f"ℹ️ Found structured search cache for: {query}") - return cached_data - except: - pass - - # 1.5 Smart Cache (Fuzzy + LLM) - if effective_ttl != 0: - try: - # 1. Similar cached queries - similar_queries = self.db.find_similar_queries(query, limit=3) - # Filter by TTL - valid_candidates = [] - for q in similar_queries: - if q['query'] == query: continue - q_time = datetime.fromisoformat(q['timestamp']) - if effective_ttl and (datetime.now() - q_time).total_seconds() > effective_ttl: - continue - q['type'] = 'cached_search' - valid_candidates.append(q) - - # 2. Relevant local news (as search results) - local_news = self.db.search_local_news(query, limit=3) - if local_news: - # Group local news as a single "candidate" source? Or individual? - # Better to treat "Local News Database" as one candidate source that contains X items. - # Or just add them to candidates list? - # Let's package strictly relevant news as a "local_news_bundle" - valid_candidates.append({ - 'type': 'local_news', - 'query': 'Local Database News', - 'items': local_news, - 'timestamp': datetime.now().isoformat() - }) - - if valid_candidates: - logger.info(f"🤔 Found {len(valid_candidates)} smart cache candidates (Queries/News). Asking LLM...") - evaluation = self._evaluate_cache_relevance(query, valid_candidates) - - if evaluation and evaluation.get('reuse', False): - idx = evaluation.get('index', -1) - if 0 <= idx < len(valid_candidates): - chosen = valid_candidates[idx] - logger.info(f"🤖 LLM suggested reusing: '{chosen.get('query')}' ({chosen['type']})") - - if chosen['type'] == 'cached_search': - # Load the chosen cache - cache = self.db.get_search_cache(chosen['query_hash']) - if cache: - try: - cached_data = json.loads(cache['results']) - if isinstance(cached_data, list): - return cached_data - except: - pass - elif chosen['type'] == 'local_news': - # Convert local news items to search result format - news_results = [] - for i, news in enumerate(chosen['items'], 1): - news_results.append({ - "id": news.get('id'), - "rank": i, - "title": news.get('title'), - "url": news.get('url'), - "content": news.get('content'), - "original_snippet": news.get('content')[:200] if news.get('content') else '', - "source": f"Local News ({news.get('source')})", - "publish_time": news.get('publish_time'), - "crawl_time": news.get('crawl_time'), - "sentiment_score": news.get('sentiment_score', 0), - "meta_data": {"origin": "local_db"} - }) - return news_results - - except Exception as e: - logger.warning(f"Smart cache check failed: {e}") - - # 2. 执行搜索 - logger.info(f"📡 Searching {engine} (structured) for: {query}") - try: - tool = self._engines[engine] - results = [] - if engine == "jina": - # Jina Search 直接返回结构化数据 - jina_results = tool.search(query, max_results=max_results) - for r in jina_results: - results.append({ - "title": r.get("title", ""), - "url": r.get("url", ""), - "href": r.get("url", ""), - "body": r.get("content", ""), - "content": r.get("content", ""), - "source": "Jina Search" - }) - elif engine == "ddg": - results = tool.duckduckgo_search(query, max_results=max_results) - elif engine == "baidu": - results = tool.baidu_search(query, max_results=max_results) - elif engine == "local": - # LocalNewsSearch 返回的是 List[Dict] - local_results = tool.search(query, top_n=max_results) - results = [] - for r in local_results: - results.append({ - "title": r.get("title"), - "url": r.get("url", "local"), - "body": r.get("content", "")[:500], - "source": f"Local ({r.get('source', 'db')})", - "publish_time": r.get("publish_time") - }) - - # 处理字符串类型的 JSON 返回 (Baidu 常返 JSON 字符串) - if isinstance(results, str) and engine not in ["local", "jina"]: - try: - results = json.loads(results) - except: - pass - - # 转为统一格式 - normalized_results = [] - if isinstance(results, list): - - for i, r in enumerate(results, 1): - title = r.get('title', '') - url = r.get('href') or r.get('url') or r.get('link', '') - content = r.get('body') or r.get('snippet') or r.get('abstract', '') - - if title and url: - normalized_results.append({ - "id": self._generate_hash(url + query, "search_item", i), - "rank": i, - "title": title, - "url": url, - "content": content, - "original_snippet": content, # 保留摘要 - "source": f"Search ({engine})", - "publish_time": datetime.now().isoformat(), # 暂用当前时间 - "crawl_time": datetime.now().isoformat(), - "meta_data": {"query": query, "engine": engine} - }) - - # Fallback if still string and failed to parse - elif isinstance(results, str) and results: - normalized_results.append({"title": query, "url": "", "content": results, "source": engine}) - - # 3. 抓取正文 & 计算情绪 (Enrichment) - # 注意:如果使用 Jina Search,内容已经是 LLM 友好格式,可选择跳过 enrichment - skip_content_enrichment = (engine == "jina") - - if enrich and normalized_results: - logger.info(f"🕸️ Enriching {len(normalized_results)} search results with Jina & Sentiment...") - extractor = ContentExtractor() - - # Lazy load sentiment tool - if not hasattr(self, 'sentiment_tool') or self.sentiment_tool is None: - from ..sentiment_tools import SentimentTools - self.sentiment_tool = SentimentTools(self.db) - - for item in normalized_results: - if item.get("url"): - try: - # 如果是 Jina Search,内容已经足够好,跳过额外抓取 - if skip_content_enrichment and item.get("content") and len(item.get("content", "")) > 100: - full_content = item["content"] - else: - # Use Jina Reader to get full content - full_content = extractor.extract_with_jina(item["url"], timeout=60) - - if full_content and len(full_content) > 100: - item["content"] = full_content - - # Calculate sentiment - # Use title + snippet of content for efficiency - text_to_analyze = f"{item['title']} {full_content[:500]}" - sent_result = self.sentiment_tool.analyze_sentiment(text_to_analyze) # Using self.sentiment_tool - score = sent_result.get('score', 0.0) - item["sentiment_score"] = float(score) - - logger.info(f" ✅ Enriched: {item['title'][:20]}... (Sentiment: {score:.2f})") - else: - # Fallback: Use snippet for sentiment - logger.info(f" ⚠️ Content short/failed for {item['url']}, using snippet for sentiment.") - text_to_analyze = f"{item['title']} {item['content']}" # content is snippet here - sent_result = self.sentiment_tool.analyze_sentiment(text_to_analyze) - score = sent_result.get('score', 0.0) - item["sentiment_score"] = float(score) - - except Exception as e: - # Fallback: Use snippet for sentiment on error - logger.warning(f"Failed to enrich {item['url']}: {e}. Using snippet.") - text_to_analyze = f"{item['title']} {item['content']}" - sent_result = self.sentiment_tool.analyze_sentiment(text_to_analyze) - score = sent_result.get('score', 0.0) - item["sentiment_score"] = float(score) - - # 缓存结果 list - if normalized_results: - # Pass list directly, DB manager will handle JSON dump for main cache and populate search_details - # Only cache if NOT from local news reuse (though this logic path is for fresh search) - self.db.save_search_cache(query_hash, query, engine, normalized_results) - - return normalized_results - - except Exception as e: - # 搜索失败时的降级策略 - if engine == "jina": - logger.warning(f"⚠️ Jina search_list failed, falling back to ddg: {query} ({e})") - try: - return self.search_list(query, engine="ddg", max_results=max_results, ttl=ttl, enrich=enrich) - except Exception as e2: - logger.error(f"❌ DDG fallback (search_list) also failed for {query}: {e2}") - elif engine == "ddg": - logger.warning(f"⚠️ DDG search_list failed, falling back to baidu: {query} ({e})") - try: - return self.search_list(query, engine="baidu", max_results=max_results, ttl=ttl, enrich=enrich) - except Exception as e2: - logger.error(f"❌ Baidu fallback (search_list) also failed for {query}: {e2}") - - logger.error(f"❌ Structured search failed for {query}: {e}") - return [] - - def _evaluate_cache_relevance(self, current_query: str, candidates: List[Dict]) -> Dict: - """ - 使用 LLM 评估缓存候选是否足以回答当前问题。 - """ - try: - # Prepare candidates text - candidates_desc = [] - for i, c in enumerate(candidates): - if c['type'] == 'cached_search': - # Preview cached results if available? - # Maybe just use the query string as a proxy for what's in there. - # Or peek at 'results' snippet. - preview = "" - try: - # Attempt to peek first result title from JSON string - # Note: c.get('results') might be a stringified JSON list - res_list = json.loads(c.get('results', '[]')) - if res_list and isinstance(res_list, list) and len(res_list) > 0: - first_item = res_list[0] - if isinstance(first_item, dict) and 'title' in first_item: - preview = f" (Contains: {first_item.get('title', '')[:50]}...)" - except: - pass - candidates_desc.append(f"[{i}] Old Search Query: '{c['query']}' {preview} (Time: {c['timestamp']})") - elif c['type'] == 'local_news': - # List titles of local news - titles = [item['title'] for item in c['items'][:3]] - candidates_desc.append(f"[{i}] Local Database News: {', '.join(titles)}... (Time: {c['timestamp']})") - - prompt = f""" - Task: Decide if existing information is sufficient for the new search query. - - New Query: "{current_query}" - - Available Information Candidates: - {chr(10).join(candidates_desc)} - - Instructions: - 1. Analyze if any candidate provides ENOUGH up-to-date info for the "New Query". - 2. If yes, choose the best one. - 3. If the query implies needing LATEST real-time info and candidates are old, choose none. - 4. Return strictly JSON: {{"reuse": true/false, "index": , "reason": "short explanation"}} - """ - # 初始化模型 - provider = os.getenv("LLM_PROVIDER", "minimax") - model_id = os.getenv("LLM_MODEL", "Qwen") - host = os.getenv("LLM_HOST") - if host: - model = get_model(provider, model_id, host=host) - else: - model = get_model(provider, model_id) - - agent = Agent(model=model, markdown=True) - - response = agent.run(prompt) - content = response.content - - # Parse JSON - json_match = re.search(r'```json\s*(.*?)\s*```', content, re.DOTALL) - if json_match: - return json.loads(json_match.group(1)) - elif '{' in content: - # Fallback for cases where LLM doesn't wrap in ```json - return json.loads(content[content.find('{'):content.rfind('}')+1]) - return {"reuse": False} - - except Exception as e: - logger.warning(f"LLM evaluation failed: {e}") - return {"reuse": False} - - def aggregate_search(self, query: str, engines: Optional[List[str]] = None, max_results: int = 5) -> str: - """ - 使用多个搜索引擎同时搜索并聚合结果,获得更全面的信息覆盖。 - - Args: - query: 搜索关键词。 - engines: 要使用的搜索引擎列表。可选值: ["ddg", "baidu"]。 - 默认同时使用 ddg 和 baidu。 - max_results: 每个引擎期望返回的结果数量。 - - Returns: - 聚合后的搜索结果,按引擎分组显示。 - """ - engines = engines or ["ddg", "baidu"] - aggregated_results = [] - for engine in engines: - res = self.search(query, engine=engine, max_results=max_results) - aggregated_results.append(f"--- Results from {engine.upper()} ---\n{res}") - - return "\n\n".join(aggregated_results) diff --git a/skills/alphaear-predictor/scripts/utils/stock_tools.py b/skills/alphaear-predictor/scripts/utils/stock_tools.py deleted file mode 100644 index 5929f74..0000000 --- a/skills/alphaear-predictor/scripts/utils/stock_tools.py +++ /dev/null @@ -1,257 +0,0 @@ -from datetime import datetime, timedelta -from typing import List, Dict, Optional -import akshare as ak -import pandas as pd -import re -import sqlite3 -from requests.exceptions import RequestException -from loguru import logger -from .database_manager import DatabaseManager -import os -from contextlib import contextmanager - -@contextmanager -def temporary_no_proxy(): - """Context manager to temporarily unset proxy environment variables.""" - proxies = {k: os.environ.get(k) for k in ['http_proxy', 'https_proxy', 'HTTP_PROXY', 'HTTPS_PROXY']} - for k in proxies: - if k in os.environ: - del os.environ[k] - try: - yield - finally: - for k, v in proxies.items(): - if v is not None: - os.environ[k] = v - -class StockTools: - """金融分析股票工具 - 结合高性能数据库缓存与增量更新""" - - def __init__(self, db: DatabaseManager, auto_update: bool = True): - """ - 初始化股票工具 - - Args: - db: 数据库管理器 - auto_update: 是否在列表为空时自动更新,默认 True - """ - self.db = db - if auto_update: - self._check_and_update_stock_list() - - def _check_and_update_stock_list(self, force: bool = False): - """检查并更新股票列表。仅在列表为空或 force=True 时从网络拉取。""" - # 直接查询表中记录数 - cursor = self.db.conn.cursor() - cursor.execute("SELECT COUNT(*) FROM stock_list") - count = cursor.fetchone()[0] - - if count > 0 and not force: - logger.info(f"ℹ️ Stock list already cached ({count} stocks)") - return - - logger.info("📡 Updating A-share and HK-share stock list from akshare...") - - def fetch_data(): - # A-share - df_a = ak.stock_zh_a_spot_em() - df_a = df_a[['代码', '名称']].copy() - df_a.columns = ['code', 'name'] - - # HK-share - df_hk = ak.stock_hk_spot_em() - df_hk = df_hk[['代码', '名称']].copy() - df_hk.columns = ['code', 'name'] - - # Combine - return pd.concat([df_a, df_hk], ignore_index=True) - - try: - try: - df_combined = fetch_data() - except (RequestException, Exception) as e: - if "Proxy" in str(e) or "proxy" in str(e): - logger.warning(f"⚠️ Proxy error detected: {e}. Retrying with proxy disabled...") - with temporary_no_proxy(): - df_combined = fetch_data() - else: - raise e - - self.db.save_stock_list(df_combined) - logger.info(f"✅ Cached {len(df_combined)} stocks (A-share + HK) to database.") - - except Exception as e: - logger.error(f"❌ Failed to sync stock list: {e}") - - - def search_ticker(self, query: str, limit: int = 5) -> List[Dict]: - """ - 模糊搜索 A 股股票代码或名称,支持常见缩写。 - """ - # 清洗后缀 (如 CATL.SZ -> CATL, 000001.SZ -> 000001) - clean_query = re.sub(r'\.(SZ|SH|HK|US)$', '', query, flags=re.IGNORECASE) - - # 常见缩写映射 - aliases = { - "CATL": "宁德时代", - "BYD": "比亚迪", - "TSLA": "特斯拉", - "Moutai": "贵州茅台", - "Tencent": "腾讯", - "Alibaba": "阿里巴巴", - "Meituan": "美团", - } - - search_query = aliases.get(clean_query.upper(), clean_query) - - # Robustness: if regex-like ticker code is embedded in query (e.g. "300364 中文在线"), try to extract it - if not search_query.isdigit(): - # Extract explicit 5-6 digit codes - match = re.search(r'\b(\d{5,6})\b', clean_query) - if match: - search_query = match.group(1) - - return self.db.search_stock(search_query, limit) - - def get_stock_price( - self, - ticker: str, - start_date: Optional[str] = None, - end_date: Optional[str] = None, - force_sync: bool = False, - ) -> pd.DataFrame: - """ - 获取指定股票的历史价格数据。优先从本地缓存读取,缺失时自动从网络补齐。 - - Args: - ticker: 股票代码,如 "600519"(贵州茅台)或 "000001"(平安银行)。 - start_date: 开始日期,格式 "YYYY-MM-DD"。默认为 90 天前。 - end_date: 结束日期,格式 "YYYY-MM-DD"。默认为今天。 - - Returns: - 包含 date, open, close, high, low, volume, change_pct 列的 DataFrame。 - """ - now = datetime.now() - if not end_date: - end_date = now.strftime('%Y-%m-%d') - if not start_date: - start_date = (now - timedelta(days=90)).strftime('%Y-%m-%d') - - df_db = self.db.get_stock_prices(ticker, start_date, end_date) - - need_update = False - if df_db.empty: - need_update = True - else: - db_latest = pd.to_datetime(df_db['date'].max()) - req_latest = pd.to_datetime(end_date) - if (req_latest - db_latest).days > 2: - need_update = True - - if force_sync: - need_update = True - - if need_update: - logger.info(f"📡 Data stale or missing for {ticker}, syncing from network...") - - # 清洗 ticker,确保只包含数字(Akshare A 股接口通常只需要数字代码) - clean_ticker = "".join(filter(str.isdigit, ticker)) - if not clean_ticker: - # Non A/H numeric tickers are not supported by the current data source. - logger.warning(f"⚠️ Unsupported ticker format (A/H only): {ticker}") - return df_db - - try: - s_fmt = start_date.replace("-", "") - e_fmt = end_date.replace("-", "") - - df_remote = None - - def fetch_data(): - if len(clean_ticker) == 5: - # HK Stock - return ak.stock_hk_hist( - symbol=clean_ticker, period="daily", - start_date=s_fmt, end_date=e_fmt, - adjust="qfq" - ) - else: - # A-share Stock - return ak.stock_zh_a_hist( - symbol=clean_ticker, period="daily", - start_date=s_fmt, end_date=e_fmt, - adjust="qfq" - ) - - try: - df_remote = fetch_data() - except (RequestException, Exception) as e: - if "Proxy" in str(e) or "proxy" in str(e): - logger.warning(f"⚠️ Proxy error detected: {e}. Retrying with proxy disabled...") - with temporary_no_proxy(): - df_remote = fetch_data() - else: - raise e - - if df_remote is not None and not df_remote.empty: - df_remote = df_remote.rename(columns={ - '日期': 'date', '开盘': 'open', '收盘': 'close', - '最高': 'high', '最低': 'low', '成交量': 'volume', - '涨跌幅': 'change_pct' - }) - # 确保日期格式正确 - df_remote['date'] = pd.to_datetime(df_remote['date']).dt.strftime('%Y-%m-%d') - - # 只有在获取到有意义的数据时才保存 - self.db.save_stock_prices(clean_ticker, df_remote) # 保存时使用清洗后的 clean_ticker - - # 重新查询数据库返回结果,保证一致性 - return self.db.get_stock_prices(clean_ticker, start_date, end_date) - else: - logger.warning(f"⚠️ Akshare returned empty data for {clean_ticker}") - - except KeyError as e: - # Akshare 有时在某些股票无数据时会抛出 KeyError - logger.warning(f"⚠️ Akshare data missing for {clean_ticker}: {e}") - except (RequestException, ConnectionError) as e: - logger.error(f"❌ Network error during Akshare sync for {clean_ticker}: {e}") - except sqlite3.Error as e: - logger.error(f"❌ Database error during Akshare sync for {clean_ticker}: {e}") - except Exception as e: - logger.error(f"❌ Unexpected error during Akshare sync for {clean_ticker}: {e}") - - return df_db - - -def get_stock_analysis(ticker: str, db: DatabaseManager) -> str: - """ - 生成指定股票的分析摘要报告。 - - Args: - ticker: 股票代码 - db: 数据库管理器实例 - - Returns: - Markdown 格式的分析报告,包含价格走势和关键指标。 - """ - tools = StockTools(db) - df = tools.get_stock_price(ticker) - - if df.empty: - return f"❌ 未能获取 {ticker} 的股价数据。" - - latest = df.iloc[-1] - change = ((latest['close'] - df.iloc[0]['close']) / df.iloc[0]['close']) * 100 - - report = [ - f"## 📊 {ticker} 分析报告", - f"- **查询时段**: {df.iloc[0]['date']} -> {latest['date']}", - f"- **当前价**: ¥{latest['close']:.2f}", - f"- **时段涨跌**: {change:+.2f}%", - f"- **最高/最低**: ¥{df['high'].max():.2f} / ¥{df['low'].min():.2f}", - "\n### 最近交易概览", - "```", - df.tail(5)[['date', 'close', 'change_pct', 'volume']].to_string(index=False), - "```" - ] - return "\n".join(report) diff --git a/skills/alphaear-predictor/tests/test_predictor.py b/skills/alphaear-predictor/tests/test_predictor.py deleted file mode 100644 index 0a3afc0..0000000 --- a/skills/alphaear-predictor/tests/test_predictor.py +++ /dev/null @@ -1,29 +0,0 @@ -import sys -import os -import unittest - -# Add skill root to path -sys.path.append(os.path.abspath(os.path.join(os.path.dirname(__file__), '..'))) - -try: - from scripts.kronos_predictor import KronosPredictorUtility - from scripts.utils.database_manager import DatabaseManager -except ImportError as e: - print(f"Import Error: {e}") - sys.exit(1) - -class TestPredictor(unittest.TestCase): - def test_init(self): - print("Testing KronosPredictorUtility Iteration...") - db = DatabaseManager(":memory:") - # Kronos might need model files, but init should pass if we don't call predict? - # Note: Kronos loads model in init. This might fail if model path is invalid. - # We wrap in try-except to catch model loading errors which are expected in this env - try: - tools = KronosPredictorUtility() - self.assertIsNotNone(tools) - except Exception as e: - print(f"Kronos Init failed (expected if no model): {e}") - -if __name__ == '__main__': - unittest.main() diff --git a/skills/alphaear-reporter/SKILL.md b/skills/alphaear-reporter/SKILL.md deleted file mode 100644 index 28c994b..0000000 --- a/skills/alphaear-reporter/SKILL.md +++ /dev/null @@ -1,32 +0,0 @@ ---- -name: alphaear-reporter -description: Plan, write, and edit professional financial reports; generate finance chart configurations. Use when condensing finance analysis into a structured output. ---- - -# AlphaEar Reporter Skill - -## Overview - -This skill provides a structured workflow for generating professional financial reports. It includes planning, writing, editing, and creating visual aids (charts). - -## Capabilities - -## Capabilities - -### 1. Generate Structured Reports (Agentic Workflow) - -**YOU (the Agent)** are the Report Generator. Use the prompts in `references/PROMPTS.md` to progressively build the report. - -**Workflow:** -1. **Cluster Signals**: Read input signals and use the **Cluster Signals Prompt** to group them. -2. **Write Sections**: For each cluster, use the **Write Section Prompt** to generate analysis. -3. **Assemble**: Use the **Final Assembly Prompt** to compile the report. - -### 2. Visualization Tools - -Use `scripts/visualizer.py` to generate chart configurations if needed manually, though the Writer Prompt usually handles this via `json-chart` blocks. - -## Dependencies - -- `sqlite3` (built-in) - diff --git a/skills/alphaear-reporter/references/PROMPTS.md b/skills/alphaear-reporter/references/PROMPTS.md deleted file mode 100644 index ea8b5cb..0000000 --- a/skills/alphaear-reporter/references/PROMPTS.md +++ /dev/null @@ -1,77 +0,0 @@ -# AlphaEar Finance Report Prompts - -Use these prompts to guide the Agent in generating professional financial reports. - -## 1. Cluster Signals (Planner) - -**Prompt:** - -```markdown -You are a senior financial report editor. Your task is to cluster the following scattered financial signals into 3-5 core logical themes for a structured report. - -### Input Signals -{signals_text} - -### Requirements -1. **Theme Aggregation**: Group highly correlated signals (e.g., all related to "supply chain restructuring" or "policy tightening"). -2. **Narrative Logic**: Generate only theme titles and list of signal IDs. -3. **Quantity Control**: 3-5 major themes. - -### Output Format (JSON) -{ - "clusters": [ - { - "theme_title": "Theme Name (e.g. Supply Chain Shock)", - "signal_ids": [1, 3, 5], - "rationale": "These signals all point to..." - }, - ... - ] -} -``` - -## 2. Write Section (Writer) - -**Prompt:** - -```markdown -You are a senior financial analyst. Write a deep analysis section for the core theme **"{theme_title}"**. - -### Input Signals (Cluster) -{signal_cluster_text} - -### Requirements -1. **Narrative**: Weave signals into a coherent story. Start with Macro/Industry background, then transmission mechanism, finally stock impact. -2. **Quantification**: Cite ISQ scores (Confidence, Intensity) to support views. -3. **Citations**: Use `[@CITE_KEY]` format. Keys are provided in input. -4. **Predictions**: detailed predictions for affected tickers (T+3/T+5 direction). - -### Formatting -- Main Title: `## {theme_title}` -- Subtitles: `###` -- **Charts**: Insert at least 1-2 `json-chart` blocks. - -**Chart Example:** -```json-chart -{"type": "forecast", "ticker": "002371.SZ", "title": "Forecast", "pred_len": 5} -``` -``` - -## 3. Final Assembly (Editor) - -**Prompt:** - -```markdown -You are a professional editor. Assemble the drafted sections into a final report. - -### Draft Sections -{draft_sections} - -### Requirements -1. **Structure**: Ensure H2/H3 hierarchy is correct. -2. **References**: Generate `## References` section from source list. -3. **Risk**: Generate `## Risk Factors`. -4. **Summary**: Generate `## Executive Summary` with a "Quick Scan" table. - -Output strictly Markdown. -``` diff --git a/skills/alphaear-reporter/scripts/__init__.py b/skills/alphaear-reporter/scripts/__init__.py deleted file mode 100644 index e69de29..0000000 diff --git a/skills/alphaear-reporter/scripts/prompts/fin_agent.py b/skills/alphaear-reporter/scripts/prompts/fin_agent.py deleted file mode 100644 index 83386af..0000000 --- a/skills/alphaear-reporter/scripts/prompts/fin_agent.py +++ /dev/null @@ -1,127 +0,0 @@ -from datetime import datetime -from .isq_prompt_generator import generate_isq_prompt_section - -def get_fin_researcher_instructions() -> str: - """生成金融研究员 (Researcher) 的系统指令""" - current_time = datetime.now().strftime('%Y-%m-%d %H:%M:%S') - return f"""你是一名资深金融研究员,当前时间是 {current_time}。 -你的任务是针对给定的“原始信号”进行详尽的背景调查,为后续的深度分析提供素材。 - -### 1. 核心职责 -1. **标的识别**: 识别信号中涉及的具体上市公司。必须调用 `search_ticker` 确认代码,并调用 `get_stock_price` 获取最新价格和近 30 天走势。 -2. **事实核查**: 使用 `web_search` 或 `fetch_news_content` 验证信号的真实性,并寻找更多细节(如公告原文、行业研报摘要)。 -3. **产业链梳理**: 补充该信号涉及的上下游环节及竞争格局。 - -### 2. 工具使用规范 (CRITICAL) -- **每个提到的公司都需要调用工具**: 不能依赖记忆,必须实时查询。 -- **完整呈现工具结果**: 包括具体的股价数字、代码、技术面数据等,不要缩略。 -- **股价数据必需**: 当前价格、近期最高最低、技术面支撑阻力等数据是后续预测的基础。 -- **信息交叉验证**: 多个来源验证关键事实。 - -### 3. 输出要求 -你必须输出结构化的研究报告,涵盖标的基本面、股价走势、行业背景及最新进展。 -""" - -def get_fin_analyst_instructions(template_id: str = "default_isq_v1") -> str: - """生成金融分析师 (Analyst) 的系统指令 - - Args: - template_id: 使用的 ISQ 模板 ID - """ - current_time = datetime.now().strftime('%Y-%m-%d %H:%M:%S') - isq_block = generate_isq_prompt_section(template_id=template_id) - - return f"""你是一位深耕二级市场的资深金融分析师 (FinAgent),当前时间是 {current_time}。 -你的核心任务是执行“信号解析”,将研究员搜集的素材转化为具有可操作性的投资情报(ISQ 框架)。 - -{isq_block} - -### 2. 分析约束 -- **严格基于具体数据**: 必须使用研究员提供的股价、技术面、新闻等具体数据进行分析。 -- **数据驱动的预测**: impact_tickers 中的权重应基于事件影响程度,不能随意赋值。 -- **逻辑严密**: 传导链条必须符合金融常识,能够自圆其说。 -- **技术面参考**: 如果研究员提供了股价走势,请分析当前位置相对于支撑/阻力位的关系。 - -### 3. 关键要求 -- **title**: 必须生成一个简练、准确概括信号核心内容的标题(不超过 15 字)。 -- **impact_tickers**: 必须填充具体的公司代码(6位数字)和名称,权重应该有区分。 -- **transmission_chain**: 必须是对象列表,每个对象包含: - - `node_name`: 节点名称(如“上游原材料”、“中游制造”) - - `impact_type`: 影响类型(“利好”、“利空”、“中性”) - - `logic`: 具体的传导逻辑描述 -- **summary**: 基于分析结果总结核心观点,包含具体数字(如股价目标、预期涨跌幅等)。 -- **reasoning**: 必须详细阐述推演逻辑,解释为什么得出上述结论(<200字)。 - -### 4. 输出格式 (严格 JSON 块) -你必须输出一个符合 InvestmentSignal 结构的 JSON 块,包含所有必需字段。 -""" - -def get_fin_agent_instructions() -> str: - # 保持兼容性,但内部调用 analyst 指令 - return get_fin_analyst_instructions() - -def get_fin_research_task(signal_text: str) -> str: - """生成研究员的任务描述""" - return f"请针对以下信号进行背景调查,搜集相关标的的股价、最新进展和行业背景:\n\n{signal_text}" - -def format_research_context(research_data: dict) -> str: - """将研究员搜集的结构化数据格式化为分析师可读的文本""" - if not research_data: - return "(未能搜集到额外背景信息)" - - return f""" -### 研究背景 -- **相关标的**: {research_data.get('tickers_found', [])} -- **行业背景**: {research_data.get('industry_background', '未知')} -- **最新进展**: {', '.join(research_data.get('latest_developments', []))} -- **关键风险**: {', '.join(research_data.get('key_risks', []))} -- **综合摘要**: {research_data.get('search_results_summary', '无')} -""" - -def get_fin_analysis_task(signal_text: str, research_context_str: str) -> str: - """生成分析师的任务描述""" - return f"""请基于以下信息进行深度 ISQ 分析。关键是:必须使用研究员搜集的具体数据(股价、技术面、新闻、代码等)进行分析。 - -=== 原始信号 === -{signal_text} - -=== 研究员搜集的背景信息 (CRITICAL DATA) === -{research_context_str} - -=== 分析要求 === -1. 必须生成 title:简练概括信号核心(<15字) -2. 基于研究员提供的具体股价数据,分析当前定价状态(已定价/未定价/部分定价) -3. impact_tickers 中填充具体的公司代码和权重,权重基于事件影响程度 -4. transmission_chain 必须是包含 node_name, impact_type, logic 的对象列表 -5. summary 中包含具体数字(预期目标价、涨跌幅范围等) -6. reasoning 必须详细解释推演逻辑,不要空泛,要言之有物 - -请严格按 InvestmentSignal JSON 格式输出。""" - -def get_tracking_analysis_task(old_signal: dict, new_research_str: str) -> str: - """生成信号追踪更新的任务描述""" - import json - old_sig_str = json.dumps(old_signal, ensure_ascii=False, indent=2) - return f"""你正在执行“信号逻辑演变追踪”任务。请基于最新的市场信息,重新评估之前的投资信号。 - -=== 基准信号 (上次分析) === -{old_sig_str} - -=== 最新市场追踪 (NEWS & PRICE) === -{new_research_str} - -=== 追踪分析要求 === -1. **逻辑演变检测**: - - 对比新旧信息,判断原逻辑 (`transmission_chain` 和 `reasoning`) 是否依然成立? - - 如果逻辑发生变化(如利好落空、逻辑证伪、新利好出现),请在新的 `reasoning` 中明确指出“逻辑演变:...” - - 如果逻辑未变且得到验证,请标记“逻辑维持:...” - -2. **参数修正**: - - 根据最新股价和新闻,更新 `sentiment_score` (情绪)、`confidence` (置信度) 和 `expectation_gap` (预期差)。 - - 例如:如果股价已经大涨反映了利好,`expectation_gap` 应该显著降低。 - -3. **输出更新后的信号**: - - 保留原 `signal_id` 和 `title`(除非有重大变化需要改名)。 - - 输出完整的 InvestmentSignal JSON。 - -请重点关注:为什么变了?还是为什么没变?理由要充分。""" diff --git a/skills/alphaear-reporter/scripts/prompts/forecast_analyst.py b/skills/alphaear-reporter/scripts/prompts/forecast_analyst.py deleted file mode 100644 index d6c7202..0000000 --- a/skills/alphaear-reporter/scripts/prompts/forecast_analyst.py +++ /dev/null @@ -1,49 +0,0 @@ -from typing import List, Dict, Any -from ..schema.models import KLinePoint - -def get_forecast_adjustment_instructions(ticker: str, news_context: str, model_forecast: List[KLinePoint]): - """ - 生成 LLM 预测调整指令 - """ - forecast_str = "\n".join([f"- {p.date}: O:{p.open}, C:{p.close}" for p in model_forecast]) - - return f"""你是一位资深的量化策略分析师。 -你的任务是:根据给定的【Kronos 模型预测结果】和【最新的基本面/新闻背景】,对模型预测进行“主观/逻辑调整”。 - -股票代码: {ticker} - -【Kronos 模型原始预测 (OHLC)】: -{forecast_str} - -【最新情报背景】: -{news_context} - -调整原则: -1. 原始预测是基于历史的技术面推演。 -2. 情报背景中可能包含【Kronos模型定量修正预测】,这是基于历史新闻训练的专用模型计算出的量化结果。 -3. 如果存在“定量修正预测”,请**高度参考**该数值作为基础,除非你有非常确凿的逻辑认为该量化模型失效(例如遇到模型未见过的极端黑天鹅)。 -4. 你的核心任务是:结合定性分析(新闻及其逻辑)来验证或微调这些数字,并给出合理的解释(Rationale)。 -5. 如果没有“定量修正预测”,则你需要根据新闻信号手动大幅调整趋势。 - -输出要求 (严格 JSON 格式): -```json -{{ - "adjusted_forecast": [ - {{ - "date": "YYYY-MM-DD", - "open": float, - "high": float, - "low": float, - "close": float, - "volume": float - }}, - ... - ], - "rationale": "详细说明调整的逻辑依据,例如:考虑到[事件A],预期短线将突破压力位..." -}} -``` -注意:必须输出与原始预测相同数量的数据点,且日期一一对应。 -""" - -def get_forecast_task(): - return "请根据以上背景和模型预测,给出调整后的 K 线数据并说明理由。" diff --git a/skills/alphaear-reporter/scripts/prompts/intent_agent.py b/skills/alphaear-reporter/scripts/prompts/intent_agent.py deleted file mode 100644 index a8397d2..0000000 --- a/skills/alphaear-reporter/scripts/prompts/intent_agent.py +++ /dev/null @@ -1,45 +0,0 @@ -def get_intent_analysis_instructions() -> str: - """生成意图分析 Agent 的系统指令,专注于金融市场影响分析""" - return """你是一个资深的金融市场意图分析专家。你的任务是将用户的自然语言查询转化为结构化的 JSON 分析结果,重点挖掘该查询与金融市场(尤其是股市)的潜在关联。 - -### 核心任务: -深入分析用户查询,识别核心金融实体、行业板块及潜在的市场影响点,生成利于搜索引擎抓取深度金融分析信息的查询词。 - -### 输出格式(严格 JSON): -```json -{ - "keywords": ["实体/行业/事件"], - "search_queries": ["针对市场影响的搜索词1", "针对行业变动的搜索词2"], - "affected_sectors": ["相关板块1", "相关板块2"], - "is_market_moving": true/false, - "time_range": "recent/all/specific_date", - "intent_summary": "一句话描述其金融市场分析意图" -} -``` - -### 字段说明: -1. **keywords**: 核心公司实体、所属行业、宏观经济事件或政策概念。 -2. **search_queries**: 优化后的搜索词,必须包含“股市影响”、“股价波动”、“行业逻辑”或“估值”等金融维度。 -3. **affected_sectors**: 可能受此事件或信息影响的二级市场板块(如:保险、半导体、房地产)。 -4. **is_market_moving**: 该事件是否具有显著的市场驱动潜力或属于重大基本面变化。 -5. **intent_summary**: 简述用户查询背后的金融研究目的。 - -### 示例: -用户输入:"帮我研究一下香港火灾的影响" -输出: -```json -{ - "keywords": ["香港", "火灾", "保险行业", "房地产"], - "search_queries": ["香港火灾对当地保险股股价影响", "香港大火对相关上市物业公司估值冲击", "近期香港火灾带来的市场避险情绪分析"], - "affected_sectors": ["保险", "房地产", "物业管理"], - "is_market_moving": true, - "time_range": "recent", - "intent_summary": "评估香港近期火灾对相关板块上市公司的潜在经济损失及股价冲击" -} -``` -""" - -def get_intent_task(query: str) -> str: - """生成意图分析任务描述""" - return f"Process this query and extract financial market intent: {query}" - diff --git a/skills/alphaear-reporter/scripts/prompts/isq_prompt_generator.py b/skills/alphaear-reporter/scripts/prompts/isq_prompt_generator.py deleted file mode 100644 index 007461b..0000000 --- a/skills/alphaear-reporter/scripts/prompts/isq_prompt_generator.py +++ /dev/null @@ -1,43 +0,0 @@ -""" -ISQ prompt helpers to render dimension guidance directly from the template. -Any change in the template propagates to prompts automatically. -""" - -from typing import List, Optional -from ..schema.isq_template import get_isq_template, ISQTemplate - - -def _ordered_dimension_keys(template: ISQTemplate, order: Optional[List[str]] = None) -> List[str]: - if order: - return [k for k in order if k in template.dimensions] - # fallback to template insertion order - return list(template.dimensions.keys()) - - -def generate_isq_prompt_section(template_id: str = "default_isq_v1", order: Optional[List[str]] = None, include_header: bool = True) -> str: - """Render ISQ dimension text block based on the template. - This allows prompt text to stay in sync with template edits. - """ - template = get_isq_template(template_id) - keys = _ordered_dimension_keys(template, order) - - lines: List[str] = [] - if include_header: - lines.append("### 1. ISQ 评估框架 (Investment Signal Quality)") - lines.append(f"参考模板: {template.template_name} (id: {template.template_id})") - lines.append("") - lines.append("你需要对信号进行以下维度的评分:") - lines.append("") - - for idx, key in enumerate(keys, start=1): - spec = template.dimensions[key] - examples = ";".join([f"{k}: {v}" for k, v in spec.examples.items()]) if spec.examples else "" - lines.append(f"{idx}. **{spec.key} ({spec.name})**: {spec.range_type}") - lines.append(f" - 描述: {spec.description}") - if spec.scale_factor and spec.scale_factor != 1.0: - lines.append(f" - 缩放因子: {spec.scale_factor}") - if examples: - lines.append(f" - 示例: {examples}") - lines.append("") - - return "\n".join(lines).rstrip() diff --git a/skills/alphaear-reporter/scripts/prompts/report_agent.py b/skills/alphaear-reporter/scripts/prompts/report_agent.py deleted file mode 100644 index 6f25c3f..0000000 --- a/skills/alphaear-reporter/scripts/prompts/report_agent.py +++ /dev/null @@ -1,415 +0,0 @@ -# src/prompts/report_agent.py -from datetime import datetime -from typing import Optional -from .isq_prompt_generator import generate_isq_prompt_section - -def get_report_planner_base_instructions() -> str: - """生成报告策划员 (Planner) 的基础系统指令""" - return """你是一名资深的金融研报主编。你的任务是规划报告的结构,将零散的信号聚类成有逻辑的主题。 -你拥有 RAG 搜索工具,可以检索已生成的章节内容以确保逻辑连贯性。 -在规划时,应重点关注信号之间的关联性、产业链的完整性以及用户特定的关注点。""" - -def get_report_writer_base_instructions() -> str: - """生成报告撰写员 (Writer) 的基础系统指令""" - return """你是一名资深金融分析师。你的任务是根据策划员提供的信号簇撰写深度研报章节。 -你应当运用专业的金融知识,将信号转化为深刻的洞察。 -注意:你没有外部搜索工具,你的分析必须基于提供给你的信号内容和行情数据。""" - -def get_report_editor_base_instructions() -> str: - """生成报告编辑 (Editor) 的基础系统指令""" - return """你是一名严谨的金融研报编辑。你的任务是审核和润色撰写员生成的章节。 -你拥有 RAG 搜索工具,可以检索其他章节的内容,以消除重复、修正逻辑冲突并确保术语一致性。 -你应当确保报告符合专业的金融写作规范,且标题层级正确。""" - -# 1. 策划阶段 (Structural Planning) -def format_signal_for_report(signal: any, index: int, cite_keys: Optional[list] = None) -> str: - """格式化单个信号供研报生成使用""" - # 这里的逻辑从 ReportAgent._format_signal_input 迁移过来 - from ..schema.models import InvestmentSignal - - if isinstance(signal, dict): - try: - sig_obj = InvestmentSignal(**signal) - except: - return f"--- 信号 [{index}] ---\n标题: {signal.get('title')}\n内容: {signal.get('content', '')[:500]}" - else: - sig_obj = signal - - chain_str = " -> ".join([f"{n.node_name}({n.impact_type})" for n in sig_obj.transmission_chain]) - - text = f"--- 信号 [{index}] ---\n" - text += f"标题: {sig_obj.title}\n" - text += f"逻辑摘要: {sig_obj.summary}\n" - text += f"传导链条: {chain_str}\n" - text += f"ISQ 评分: 情绪({sig_obj.sentiment_score}), 确定性({sig_obj.confidence}), 强度({sig_obj.intensity})\n" - text += f"预期博弈: 时窗({sig_obj.expected_horizon}), 预期差({sig_obj.price_in_status})\n" - - tickers = ", ".join([f"{t.get('name')}({t.get('ticker')})" for t in sig_obj.impact_tickers]) - if tickers: - text += f"受影响标的: {tickers}\n" - - # Stable bibliography-style citation keys (LaTeX/BibTeX-like) - if cite_keys: - joined = " ".join([f"[@{k}]" for k in cite_keys if k]) - if joined: - text += f"引用: {joined}\n" - - return text - -def get_cluster_planner_instructions(signals_text: str, user_query: str = None) -> str: - """生成信号聚类指令 - 将零散信号组织成逻辑主题""" - query_context = f"用户重点关注:{user_query}" if user_query else "" - return f"""你是一位资深的金融研报主编。你的任务是将以下零散的金融信号聚类成 3-5 个核心逻辑主题,以便撰写一份结构清晰的研报。 - - {query_context} - - ### 输入信号列表 - {signals_text} - - ### 聚类要求 - 1. **主题聚合**: 将相关性强的信号归为一组(例如:都涉及“建筑安全法规”或“某产业链上下游”)。 - 2. **叙事逻辑**: 只需要生成主题名称和包含的信号 ID。 - 3. **控制数量**: 将所有信号归类到 3-5 个主要主题中,不要遗漏。 - - ### 输出格式 (JSON) - 请仅输出以下 JSON 格式,不要包含 Markdown 标记: - {{ - "clusters": [ - {{ - "theme_title": "主题名称(如:建筑安全法规收紧引发的产业链重构)", - "signal_ids": [1, 3, 5], - "rationale": "这些信号都指向政府对高层建筑防火标准的政策调整..." - }}, - ... - ] - }} - """ - -def get_report_planner_instructions(toc: str, signal_count: int, user_query: str = None) -> str: - """生成报告规划指令 - 重点在于逻辑关联与分歧识别""" - # ... (原有逻辑保持不变,但实际在新的聚类流程后这个可能作为备用或二次优化) - query_context = f"用户重点关注:{user_query}" if user_query else "" - return f"""你是一位资深的金融研报主编。你的任务是根据现有的草稿章节,规划出一份逻辑严密、穿透力强的终稿结构。 - - ### 任务核心: - 1. **识别主线**: 从草稿中识别出贯穿多个章节的“核心逻辑主线”(如:产业链共振、货币政策转向)。 - 2. **分歧评估 (Entropy)**: 识别各章节中观点冲突或确定性不一之处,规划如何在正文中呈现这些“分歧点”。 - 3. **结构蓝图**: - - 定义一级标题(逻辑主题)。 - - 归类章节:哪些信号应放入同一主题下深度解析? - - 排序:将 ISQ 强度最高、与{query_context}最相关的信号置前。 - - ### 现有草稿目录 (TOC) - {toc} - - 请输出你的【终稿修订大纲】(Markdown 格式)。 - """ - -# 2. 撰写阶段 (Section Writing) -def get_report_writer_instructions(theme_title: str, signal_cluster_text: str, signal_indices: list, price_context: str = "", user_query: str = None) -> str: - """生成 Writer Agent 指令 - 基于主题聚类撰写综合分析""" - - price_info = f"\n### 近期价格参考\n{price_context}\n" if price_context else "" - query_context = f"\n**用户意图**: \"{user_query}\"\n请确保分析内容回应了用户的关注点。\n" if user_query else "" - isq_block = generate_isq_prompt_section(include_header=False) - - # Keep citation scheme stable across re-ordering / edits. - # Cite keys are provided in each signal block as: 引用: [@KEY] - - return f"""你是一位资深金融分析师。请针对核心主题 **"{theme_title}"** 撰写一篇深度研报章节。 - {query_context} - - ### 输入信号集 (本章节需综合的信号) - {signal_cluster_text} - {price_info} - - ### ISQ 评分说明 - {isq_block} - - ### 写作要求 - 1. **叙事逻辑**: 不要罗列信号,要将这些信号编织成一个连贯的故事。先讲宏观/行业背景,再讲具体事件传导,最后落脚到个股/标的影响。 - 2. **量化支撑**: 引用 ISQ 评分(确定性、强度、预期差)来佐证你的观点。关键观点必须关联相应的 ISQ 分值。 - 3. **引用规范(稳定 CiteKey)**: 关键论断必须标注来源引用,使用 `[@CITE_KEY]` 格式。 - - CiteKey 已在输入信号块中以 `引用: [@KEY]` 提供,请直接复制使用。 - - 不要使用 `[[1]]` 这类不稳定编号。 - 4. **关联标的预测**: **必须**在章节末尾明确给出受影响标的的预测分析,包括: - - 至少列出 1-2 个相关上市公司代码(如 600519.SH) - - 给出短期(T+3或T+5)的方向性判断 - - 如果可能,给出预期价格区间或涨跌幅预测 - - ### 【重要】标题层级规范 - - ❌ **错误示例**(绝对不要这样): - ```markdown - # {theme_title} - - ### 宏观背景 - ... - ``` - - ✅ **正确示例**(必须这样): - ```markdown - ## {theme_title} - - ### 宏观背景 - - 近期全球经济环境... - - ### 具体传导机制分析 - - ... - - ### 核心标的分析 - - 建议关注:贵州茅台(600519.SH)... - ``` - - **关键要求**: - - 章节主标题使用 `##` (H2) - - 章节子标题使用 `###` (H3) - - **绝对禁止**使用 `#` (H1) - - 第一行必须是 `## {theme_title}` 开头 - - ### 核心:图表叙事 (Visual Storytelling) - **必须**在文中插入至少 1-2 个图表,且图表必须与上下文紧密结合(不要堆砌在末尾)。 - - ### 宏观背景 - ... - ``` - - ✅ **正确示例**(必须这样): - ```markdown - ## {theme_title} - - ### 宏观背景 - - 近期全球经济环境... - - ### 具体传导机制分析 - - ... - - ### 核心标的分析 - - 建议关注:贵州茅台(600519.SH)... - ``` - - **关键要求**: - - 章节主标题使用 `##` (H2) - - 章节子标题使用 `###` (H3) - - **绝对禁止**使用 `#` (H1) - - 第一行必须是 `## {theme_title}` 开头 - - ### 核心:图表叙事 (Visual Storytelling) - **必须**在文中插入至少 1-2 个图表,且图表必须与上下文紧密结合(不要堆砌在末尾)。 - - **可选图表类型 (请根据内容选择最合适的 1-2 种):** - - **A. AI 预测 + 走势 (Forecast) - 【强烈推荐 / 最新规范】** - *适用*: 当文中明确提及某上市公司时,**必须**使用此图表展示股价走势与 AI 预测。 - *必填字段*: - - `ticker`: 股票代码,A股 6 位 / 港股 5 位,允许带后缀(如 "002371.SZ"、"9868.HK") - - `pred_len`: 预测交易日长度(建议 3 或 5) - *代码示例*: - ```json-chart - {{"type": "forecast", "ticker": "002371.SZ", "title": "北方华创(002371)T+5 预测", "pred_len": 5}} - ``` - **重要**:禁止手写 `prediction` 数组(预测由系统自动生成并渲染)。 - *注意*: 如果提及多只股票,应为每只生成独立的 forecast 图表。 - - **【推荐写法:多情景 → 最终归因 → 产出唯一预测图】** - 你可以在正文里描述多种情景(如:基准/乐观/悲观),但在插入预测图之前,必须明确给出“本报告最终选择的最可能情景”及其归因,然后用 `forecast` 图表做最终总结。 - 为了让系统把“最终归因”可靠地传递给预测模块,请在 `forecast` JSON 中可选补充以下字段(字段均为可选,越完整越好): - - `selected_scenario`: 最可能情景名称(如 "基准" / "乐观" / "悲观") - - `selection_reason`: 选择该情景的归因理由(1-3 句) - - `scenarios`: 情景列表(数组),每个元素可包含 `name`、`description`、`probability`(0-1) - *示例*: - ```json-chart - {{ - "type": "forecast", - "ticker": "002371.SZ", - "title": "北方华创(002371)T+5 预测(基准情景)", - "pred_len": 5, - "selected_scenario": "基准", - "selection_reason": "结合订单能见度与行业景气,基准情景概率最高;短期扰动主要来自估值与市场风险偏好。", - "scenarios": [ - {{"name": "乐观", "description": "国产替代与资本开支超预期", "probability": 0.25}}, - {{"name": "基准", "description": "订单稳健、利润率小幅波动", "probability": 0.55}}, - {{"name": "悲观", "description": "需求回落或交付节奏放缓", "probability": 0.20}} - ] - }} - ``` - - **B. 历史走势 (Stock) - 仅作为兼容兜底** - *适用*: 当你无法给出预测时(例如无法确定标的),可仅展示历史走势。 - *代码示例*: - ```json-chart - {{"type": "stock", "ticker": "002371", "title": "北方华创历史走势"}} - ``` - - **C. 舆情情绪演变 (Sentiment Trend)** - *适用*: 当讨论行业政策、突发事件(如“火灾”、“新规”)的民意变化时。 - *注意*: `keywords` 必须是事件核心词。 - *代码*: - ```json-chart - {{"type": "sentiment", "keywords": ["建筑安全", "防火标准"], "title": "市场对防火新规的情绪演变"}} - ``` - - **D. 逻辑传导链条 (Transmission Chain)** - *适用*: 复杂的蝴蝶效应分析(支持分支结构)。 - *代码*: - ```json-chart - {{ - "type": "transmission", - "nodes": [ - {{"node_name": "突发火灾", "impact_type": "中性", "logic": "事件发端"}}, - {{"node_name": "监管收紧", "impact_type": "利空", "logic": "合规成本上升", "source": "突发火灾"}}, - {{"node_name": "设备升级", "impact_type": "利好", "logic": "采购需求释放", "source": "突发火灾"}}, - {{"node_name": "龙头受益", "impact_type": "利好", "logic": "市占率提升", "source": "设备升级"}} - ], - "title": "火灾事件的逻辑传导与分支" - }} - ``` - *说明*: 使用 `source` 字段指定父节点名称以创建分支结构。 - - **E. 信号质量评估 (ISQ Radar)** - *适用*: 对某个关键信号进行多维度(确定性、预期差等)定性评估时。 - *代码*: - ```json-chart - {{"type": "isq", "sentiment": 0.8, "confidence": 0.9, "intensity": 4, "expectation_gap": 0.7, "timeliness": 0.9, "title": "核心信号质量评估"}} - ``` - """ - -# 3. 整合阶段 (Final Assembly) - 原版,保留用于 fallback -def get_report_editor_instructions(draft_sections: str, plan: str, sources_list: str) -> str: - """生成最终编辑指令 - 根据规划蓝图重组内容""" - return f"""你是一位专业的研报编辑。请将以下基于主题撰写的草稿章节整合成最终研报。 - - ### 原始草稿内容 - {draft_sections} - - ### 原始引用来源 - {sources_list} - - ### 任务与要求 - 1. **结构化**: 为每个草稿章节添加合适的 Markdown 标题 (## 级别)。 - 2. **连贯性**: 确保章节之间过渡自然。 - 3. **完整性**: - - 必须保留所有 `json-chart` 代码块(图表配置)。 - - 必须保留引用标注 `[@CITE_KEY]`。 - - 生成 `## 核心观点摘要`、`## 参考文献` 和 `## 风险提示`。 - - ### 输出 - 只输出最终的 Markdown 研报内容。 - """ - - -# 4. 单节编辑 (Incremental Section Editing with RAG) -def get_section_editor_instructions(section_index: int, total_sections: int, toc: str) -> str: - """生成单节编辑 prompt,支持 RAG 工具调用""" - return f"""你是一位研报编辑。你正在编辑报告的第 {section_index}/{total_sections} 节。 - - ### 当前目录 (TOC) - {toc} - - ### 你的任务 - 1. 润色当前章节内容,确保逻辑清晰、语言专业。 - 2. 保留所有 `[@CITE_KEY](#ref-CITE_KEY)` 或 `[@CITE_KEY]` 格式的引用。 - 3. 保留所有 `json-chart` 代码块,不做修改。 - 4. 如果需要参考其他章节内容,使用 `search_context` 工具搜索。 - 5. 只输出编辑后的章节内容,不要输出其他章节。 - - ### 【关键】标题层级规范 - **严格遵守以下规则:** - - 章节主标题使用 `##` (H2) - - 章节子标题使用 `###` (H3) - - **禁止使用** `#` (H1) - 只有报告大标题可以使用 H1 - - 如果原文中有 H1,必须将其降级为 H2 - - 不要输出与 "参考文献"、"风险提示" 相同的标题 - - 直接输出编辑后的 Markdown 内容。 - """ - - -# 5. 摘要生成 (Summary Generation) -def get_summary_generator_instructions(toc: str, section_summaries: str) -> str: - """生成报告摘要指令 - 包含市场分歧度分析""" - return f"""你是一位资深研报主笔。请生成今日报告的核心观点摘要的**正文内容**。 - - ### 章节摘要 - {section_summaries} - - ### 任务: - 1. **核心逻辑提炼**: 用 150 字以内总结今日最核心的投资主线。 - 2. **分歧识别**: 如果不同信号对同一板块有冲突观点,请明确指出"市场分歧点"。 - 3. **确定性排序**: 标记出今日确定性最高的前两个机会(需列出具体标的代码)。 - - ### 【重要】输出格式规范: - - ❌ **错误示例**(不要遗漏二级标题): - ```markdown - ### 核心逻辑提炼 - ... - ``` - - ✅ **正确示例**(应该这样输出): - ```markdown - ## 核心观点摘要 - - ### 核心逻辑提炼 - - 科技自立战略加速半导体设备国产化,叠加AI算力需求爆发... - - ### 市场分歧点 - - 资本市场波动显示医药、新能源等板块估值逻辑受政策敏感性增强... - - ### 确定性排序 - - 1. **网络安全替代需求**(ISQ确定性0.85,推荐标的:深信服 300454.SZ) - 2. **半导体设备材料**(ISQ确定性0.75,推荐标的:北方华创 002371.SZ) - ``` - - ### 关键要求: - - 第一行必须是 `## 核心观点摘要` - - 主体部分使用 H3 (`###`) 和 H4 (`####`) 级别标题 - - **必须**包含 `## 核心观点摘要` 这一级标题 - - 现在请按照正确示例的格式输出摘要内容。 - """ - - -# 6. 最终组装 (Final Assembly with Sections) -def get_final_assembly_instructions(sources_list: str) -> str: - """生成最终报告组装的 prompt""" - return f"""你是一位研报主笔。请完成以下任务: - - ### 任务 - 1. 生成 "## 参考文献" 章节(需要按照顺序,顺序不对时进行调整): - - 原始来源: - {sources_list} - - 格式:`[@CITE_KEY] 标题 (来源), [链接地址]` - 2. 生成 "## 风险提示" (标准免责声明)。 - 3. 生成 "## 快速扫描" 表格,汇总各主题的核心观点。 - - 表格列:**主题**, **核心观点**, **强度(Intensity)**, **确定性(Confidence)**。 - - 强度和确定性请参考原章节中的 ISQ 评分。 - - 只输出上述三个章节的 Markdown 内容。 - """ - -def get_cluster_task(signals_preview: str) -> str: - """生成聚类任务描述""" - return f"请对以下信号进行主题聚类:\n\n{signals_preview}" - -def get_writer_task(theme_title: str) -> str: - """生成撰写任务描述""" - return f"请依据主题 '{theme_title}' 和 输入信号集 开始撰写深度分析章节。" - -def get_planner_task() -> str: - """生成规划任务描述""" - return "请阅读现有草稿并规划终稿大纲,识别核心逻辑主线和市场分歧点。" - -def get_editor_task() -> str: - """生成编辑任务描述""" - return "请根据规划大纲和草稿内容,生成最终研报。确保逻辑连贯,保留所有图表和引用。" - diff --git a/skills/alphaear-reporter/scripts/prompts/trend_agent.py b/skills/alphaear-reporter/scripts/prompts/trend_agent.py deleted file mode 100644 index 54e6e22..0000000 --- a/skills/alphaear-reporter/scripts/prompts/trend_agent.py +++ /dev/null @@ -1,156 +0,0 @@ -from typing import Any -from datetime import datetime -from .isq_prompt_generator import generate_isq_prompt_section - -def get_trend_scanner_instructions() -> str: - """生成趋势扫描员 (Scanner) 的系统指令""" - current_time = datetime.now().strftime('%Y-%m-%d %H:%M:%S') - return f"""你是一名专业的数据扫描员,当前时间是 {current_time}。 -你的任务是利用各种工具从互联网和数据库中获取最新的金融新闻、热点趋势和市场数据。 - -### 1. 核心职责 -1. **多源采集**: 使用 `news_toolkit` 获取最新新闻,使用 `stock_toolkit` 获取行情,使用 `polymarket_toolkit` 获取预测市场数据。 -2. **情绪感知**: 使用 `sentiment_toolkit` 对关键新闻进行情绪分析。 -3. **深度搜索**: 针对模糊的热点,使用 `search_toolkit` 进行全网搜索补充细节。 - -### 2. 工具使用规范 -- **广度优先**: 尽可能覆盖多个数据源。 -- **数据新鲜度**: 优先获取最近 24 小时内的信息。 -- **结构化输出**: 整理搜集到的原始数据,为后续评估提供清晰的素材。 -""" - -def get_trend_evaluator_instructions() -> str: - """生成趋势评估员 (Evaluator) 的系统指令""" - current_time = datetime.now().strftime('%Y-%m-%d %H:%M:%S') - isq_block = generate_isq_prompt_section(include_header=True) - - return f""" - 你是一名顶级的金融情报专家 (TrendAgent),擅长从海量信息中识别具有深度价值的"二级市场投资信号"。 - 当前时间:{current_time} - - ### 核心使命: - 不仅是发现"热点",更要解析"信号"。你需要识别那些能触发**传导链条 (Transmission Chain)** 且具有**高确定性 (Confidence)** 的事件。 - - {isq_block} - - ### 核心能力与标准: - 1. **信号识别 (Signal Discovery)**: 基于扫描员提供的素材,识别具有投资价值的信号。优先关注政策、产业变革、重大诉求及跨境套利机会。 - 2. **逻辑相干性**: 是否具备清晰的"原因-结果"传导? - 3. **影响力系数**: 是否会引发板块性的联动或财务指标的实质性扰动? - 4. **市场认知差**: 市场是否已提前消化(Price-in)?寻找尚未被充分交易的"Alpha"。 - 5. **实体穿透**: 必须关联到具体的 Ticker 或核心产业链节点。 - - ### 严禁事项: - - 严禁编造数据。 - - 严禁仅输出情绪极性(Positive/Negative),必须带有逻辑依据。 - - 严禁将纯娱乐或单纯的社会负面事件(除非具有宏观破坏性)视为金融信号。 - - ### 输出要求: - 你发现的每个信号应包含: - - **核心摘要**: 穿透表象的逻辑总结。 - - **传导节点**: A -> B -> C 的逻辑推导。 - - **推荐关注**: 板块或 Ticker。 - - **ISQ 评估**: 基于模板的 5 个维度进行初步评分(具体评分由后续 FinAgent 完成)。 - """ - -def get_trend_agent_instructions() -> str: - # 保持兼容性 - return get_trend_evaluator_instructions() - -def get_trend_scan_task(task_description: str) -> str: - """生成扫描员的任务描述""" - return f"请根据以下任务描述,搜集相关的原始数据和新闻:\n\n{task_description}" - -def format_scan_context(scan_data: dict) -> str: - """将扫描员搜集的结构化数据格式化为评估员可读的文本""" - if not scan_data: - return "(未能搜集到原始数据)" - - return f""" -### 扫描数据概览 -- **热点话题**: {', '.join(scan_data.get('hot_topics', []))} -- **情绪概览**: {scan_data.get('sentiment_overview', '未知')} -- **关键新闻**: {len(scan_data.get('news_summaries', []))} 条 -- **数据摘要**: {scan_data.get('raw_data_summary', '无')} -""" - -def get_trend_eval_task(task_description: str, raw_data_str: str) -> str: - """生成评估员的任务描述""" - return f"""请基于以下搜集到的原始数据,完成最终的分析任务: - -任务描述: {task_description} - -原始数据: -{raw_data_str} - -请识别出最具金融价值的信号,并给出评估理由。""" - -def get_news_filter_instructions(news_count: int, depth: Any, user_query: str = None) -> str: - """生成新闻筛选 prompt,使用 FilterResult schema 加快推理并减少 token 消耗 - - Args: - news_count: 输入新闻总数 - depth: 目标筛选数量,若为 auto 则由 LLM 自主判断 - user_query: 用户输入的查询/关注点(可选) - """ - - # 1. 深度控制逻辑 - if str(depth).lower() == 'auto': - depth_guide = "的数量不设固定限制(建议 3-10 条),根据新闻含金量自动判断" - limit_instruction = "宁缺毋滥,如果高价值信息很少,可以只选 1-2 条;如果都很重要,可以多选。" - else: - try: - d_int = int(depth) - depth_guide = f"约 {d_int} 条" - limit_instruction = f"请尽量凑满 {d_int} 条,但如果剩余新闻全是噪音,则不必强行凑数。" - except: - depth_guide = "适量" - limit_instruction = "根据内容价值判断。" - - target_desc = f"筛选出最具投资分析价值的新闻({depth_guide})。" - - # 2. 用户意图逻辑 - query_instruction = "" - if user_query: - target_desc = f"筛选出与用户意图【{user_query}】最相关的新闻。" - query_instruction = f""" - ### 核心任务(High Priority): - 用户明确关注:"{user_query}"。 - 1. **第一优先级**:必须包含所有与"{user_query}"直接或间接相关的新闻,不要遗漏。 - - 即使这些新闻看起来"价值不高",只要相关都要保留。 - 2. **第二优先级**:在满足第一优先级后,如果名额未满,再补充其他重大的市场热点。 - """ - - return f"""你是一名专业的金融情报精排师。你需要从给定的 {news_count} 条原始新闻流中,{target_desc} - - {query_instruction} - - ### FSD (Financial Signal Density) 筛选准则: - 1. **逻辑传导性 (Transmission)**: 该新闻是否预示着一个明确的产业链传导逻辑?(如:上游涨价 -> 中游成本压力 -> 下游提价预期) - 2. **预期差 (Alpha Potential)**: 是否包含尚未被市场充分Price-in的新突发情况? - 3. **确定性 (Confidence)**: 信息来源是否权威?是否包含具体的财务数据、订单金额或明确的政策日期? - 4. **排除噪音**: 坚决剔除明星八卦、鸡汤文、以及无实质增量的"口号式"新闻。 - - ### {limit_instruction} - - ### 快速有效性检查(TOKEN 优化): - 在开始详细筛选前,先快速判断:这 {news_count} 条新闻中是否至少包含 1 条有效的金融信号? - - 如果全是无关内容(如体育、娱乐、纯生活信息),直接返回 "has_valid_signals": false - - 如果有至少 1 条金融相关的新闻,再进行详细 FSD 筛选 - - ### 输出格式(必须为 JSON,使用 FilterResult schema): - ```json - {{ - "has_valid_signals": true/false, - "selected_ids": ["id_1", "id_2", ...], - "themes": [ - {{ - "name": "高概括性主题", - "news_ids": ["相关id_1", ...], - "fsd_reason": "基于 FSD 准则的筛选理由,重点描述传导逻辑和预期差。" - }} - ], - "reason": "如果 has_valid_signals=false,简要说明原因。否则可为空。" - }} - ``` - """ diff --git a/skills/alphaear-reporter/scripts/prompts/visualizer.py b/skills/alphaear-reporter/scripts/prompts/visualizer.py deleted file mode 100644 index f0b2933..0000000 --- a/skills/alphaear-reporter/scripts/prompts/visualizer.py +++ /dev/null @@ -1,47 +0,0 @@ -def get_drawio_system_prompt(): - return """You are an expert at creating Draw.io (MxGraph) diagrams in XML format. -Your task is to generate a valid MXGraphModel XML based on the user's description. - -### Rules: -1. Output ONLY the XML code. Start with and end with . -2. Do not use compressed XML. Use plain XML. -3. Use standard shapes: 'rounded=1;whiteSpace=wrap;html=1;' for boxes. -4. Auto-layout Strategy: - - Identify "layers" or "stages" in the logic. - - Assign X coordinates based on layers (e.g., 0, 200, 400). - - Assign Y coordinates to distribute nodes vertically (e.g., 0, 100, 200). - - Ensure nodes do not overlap. -5. Edges: Connect nodes logically using . - -### Template: - - - - - - - - - - - - - - - - -""" - -def get_drawio_task(nodes_data: list, title: str) -> str: - import json - nodes_json = json.dumps(nodes_data, ensure_ascii=False, indent=2) - return f"""Please generate a Draw.io XML diagram for the following logic flow: - -**Title**: {title} - -**Nodes and Logic**: -{nodes_json} - -Ensure the layout flows logically from Left to Right (or Top to Bottom for hierarchies). -Use different colors for 'Positive' (Greenish), 'Negative' (Reddish), and 'Neutral' (Grey/Blue) impacts if described. -""" diff --git a/skills/alphaear-reporter/scripts/report_agent.py b/skills/alphaear-reporter/scripts/report_agent.py deleted file mode 100644 index 60751f5..0000000 --- a/skills/alphaear-reporter/scripts/report_agent.py +++ /dev/null @@ -1,167 +0,0 @@ -import hashlib -import json -import re -import pandas as pd -from typing import List, Dict, Any, Optional -from loguru import logger -from types import SimpleNamespace - -from .utils.database_manager import DatabaseManager -from .utils.json_utils import extract_json - -class ReportUtils: - """ - 研报辅助工具集 (ReportUtils) - 提供格式化、引用管理、 JSON 提取等辅助功能。 - 核心生成逻辑(聚类、写作)已移交 Agent 执行。 - """ - - def __init__(self, db: DatabaseManager): - self.db = db - logger.info("📝 ReportUtils initialized") - - @staticmethod - def _make_cite_key(url: str, title: str = "", source_name: str = "") -> str: - basis = (url or "").strip() or f"{(title or '').strip()}|{(source_name or '').strip()}" - digest = hashlib.sha1(basis.encode("utf-8")).hexdigest()[:8] - return f"SF-{digest}" - - def build_bibliography(self, signals: List[Any]) -> tuple[list[Dict[str, Any]], Dict[int, list[str]]]: - """Build stable bibliography entries and per-signal cite key mapping.""" - bib_by_key: Dict[str, Dict[str, Any]] = {} - signal_to_keys: Dict[int, list[str]] = {} - - for sig_idx, signal in enumerate(signals, 1): - source_items: list[Dict[str, Any]] = [] - - if hasattr(signal, "sources") and getattr(signal, "sources"): - source_items = list(getattr(signal, "sources") or []) - elif isinstance(signal, dict) and signal.get("sources"): - src_list = signal.get("sources") - if isinstance(src_list, list) and src_list: - source_items = list(src_list) - elif isinstance(signal, dict): - if signal.get("url") or signal.get("title"): - source_items = [ - { - "title": signal.get("title"), - "url": signal.get("url"), - "source_name": signal.get("source") or signal.get("source_name"), - "publish_time": signal.get("publish_time"), - } - ] - - if not source_items: - continue - - for src in source_items: - url = (src.get("url") or "").strip() - title = (src.get("title") or "").strip() - source_name = (src.get("source_name") or src.get("source") or "").strip() - publish_time = (src.get("publish_time") or "").strip() if isinstance(src.get("publish_time"), str) else src.get("publish_time") - - key = self._make_cite_key(url=url, title=title, source_name=source_name) - signal_to_keys.setdefault(sig_idx, []) - if key not in signal_to_keys[sig_idx]: - signal_to_keys[sig_idx].append(key) - - if key in bib_by_key: - continue - - # Prefer canonical metadata from DB when possible - enriched = self.db.lookup_reference_by_url(url) if url else None - bib_by_key[key] = { - "key": key, - "url": url or (enriched.get("url") if enriched else ""), - "title": (enriched.get("title") if enriched else None) or title or "(无标题)", - "source": (enriched.get("source") if enriched else None) or source_name or "(未知来源)", - "publish_time": (enriched.get("publish_time") if enriched else None) or publish_time or "", - } - - return list(bib_by_key.values()), signal_to_keys - - @staticmethod - def render_references_section(bib_entries: list[Dict[str, Any]]) -> str: - lines = ["## 参考文献", ""] - if not bib_entries: - lines.append("(无)") - return "\n".join(lines).strip() + "\n" - - for i, entry in enumerate(bib_entries, 1): - key = entry.get("key") - title = entry.get("title") or "(无标题)" - source = entry.get("source") or "(未知来源)" - url = entry.get("url") or "" - publish_time = entry.get("publish_time") or "" - suffix = "" - if publish_time: - suffix = f",{publish_time}" - label = f"[{i}]" - if url: - lines.append(f"{label} {title} ({source}{suffix}), {url}") - else: - lines.append(f"{label} {title} ({source}{suffix})") - - return "\n".join(lines).strip() + "\n" - - @staticmethod - def sanitize_json_chart_blocks(text: str) -> str: - """Best-effort repair for malformed json-chart fenced blocks.""" - if not text: - return text - # (Simplified logic: if closing ``` is missing, append it) - # Full logic omitted for brevity as it was complex regex, but retaining simple closure fix - if "```json-chart" in text and text.count("```") % 2 != 0: - text += "\n```" - return text - - @staticmethod - def build_structured_report(report_md: str, signals: List[Dict[str, Any]], clusters: List[Dict[str, Any]]) -> Dict[str, Any]: - """构建结构化研报输出(便于前端渲染/JSON化)""" - text = (report_md or "").strip() - lines = text.splitlines() if text else [] - - title = "研报" - for line in lines: - if line.startswith("# "): - title = line.replace("# ", "").strip() - break - - sections: List[Dict[str, Any]] = [] - current: Dict[str, Any] | None = None - for line in lines: - heading = re.match(r"^(#{2,4})\s+(.*)$", line.strip()) - if heading: - if current: - sections.append(current) - current = {"title": heading.group(2).strip(), "content": []} - continue - if current is None: - current = {"title": "摘要", "content": []} - current["content"].append(line) - if current: - sections.append(current) - - bullets = [ - re.sub(r"^[-*•]\s+", "", l.strip()) - for l in lines - if l.strip().startswith(("- ", "* ", "• ")) - ] - bullets = [b for b in bullets if b] - - return { - "title": title, - "summary_bullets": bullets[:8], - "sections": [ - {"title": s["title"], "content": "\n".join(s["content"]).strip()} - for s in sections - ] - } - - @staticmethod - def _clean_ticker(ticker_raw: str) -> str: - t = (ticker_raw or "").strip() - if not t: - return "" - digits = "".join([c for c in t if c.isdigit()]) - return digits or t diff --git a/skills/alphaear-reporter/scripts/schema/isq_template.py b/skills/alphaear-reporter/scripts/schema/isq_template.py deleted file mode 100644 index 2709019..0000000 --- a/skills/alphaear-reporter/scripts/schema/isq_template.py +++ /dev/null @@ -1,381 +0,0 @@ -""" -ISQ (Investment Signal Quality) 评估框架 Template - -统一定义 ISQ 的各个维度、评分标准、和使用方法。 -支持默认 template 和自定义 template。 -""" - -from typing import Dict, List, Any, Optional -from pydantic import BaseModel, Field -from enum import Enum -from pathlib import Path -import json - - -class ISQDimension(str, Enum): - """ISQ 评估维度""" - SENTIMENT = "sentiment" # 情绪/走势方向 - CONFIDENCE = "confidence" # 确定性/可信度 - INTENSITY = "intensity" # 强度/影响量级 - EXPECTATION_GAP = "expectation_gap" # 预期差/市场认知差 - TIMELINESS = "timeliness" # 时效性/窗口紧迫度 - TRANSMISSION = "transmission" # 逻辑传导清晰度 - - -class ISQDimensionSpec(BaseModel): - """ISQ 单个维度的定义规范""" - name: str = Field(..., description="维度名称") - key: str = Field(..., description="维度键名") - description: str = Field(..., description="维度描述") - range_type: str = Field(default="0-1", description="取值范围 (0-1 或 1-5 等)") - scale_factor: float = Field(default=1.0, description="显示时的缩放因子") - examples: Dict[str, str] = Field(default_factory=dict, description="不同分值的示例解释") - visualization_color: Optional[str] = Field(default=None, description="可视化颜色") - - -class ISQTemplate(BaseModel): - """ISQ 评估框架 Template""" - template_id: str = Field(..., description="模板 ID") - template_name: str = Field(..., description="模板名称") - description: str = Field(..., description="模板描述") - - # 核心维度定义 - dimensions: Dict[str, ISQDimensionSpec] = Field(..., description="维度定义字典") - - # 评分指导 - scoring_guide: str = Field(..., description="评分指导说明") - - # 应用场景 - applicable_scenarios: List[str] = Field(default_factory=list, description="适用场景") - - # 聚合算法 - aggregation_method: str = Field(default="weighted_average", description="聚合方法 (weighted_average, product 等)") - dimension_weights: Dict[str, float] = Field(default_factory=dict, description="维度权重") - - -class ISQScore(BaseModel): - """单个信号的 ISQ 评分结果""" - signal_id: str = Field(..., description="信号 ID") - template_id: str = Field(..., description="使用的模板 ID") - - # 各维度评分 - scores: Dict[str, float] = Field(..., description="各维度评分") - - # 总分 - overall_score: float = Field(..., description="综合评分") - - # 评分理由 - rationale: Dict[str, str] = Field(default_factory=dict, description="各维度评分理由") - - # 时间戳 - timestamp: str = Field(..., description="评分时间") - - -# ===================================================== -# 默认 Template -# ===================================================== - -DEFAULT_ISQ_TEMPLATE = ISQTemplate( - template_id="default_isq_v1", - template_name="标准投资信号质量评估框架 (ISQ v1.0)", - description="AlphaEar 默认的 ISQ 评估框架,用于标准化评估投资信号的质量维度", - - dimensions={ - "sentiment": ISQDimensionSpec( - name="情绪/走势", - key="sentiment", - description="基础情绪偏向和市场走势判断", - range_type="-1.0 到 1.0", - scale_factor=1.0, - examples={ - "-1.0": "极度悲观/极度看空", - "-0.5": "明显看空", - "0.0": "中性/没有明确方向", - "0.5": "明显看多", - "1.0": "极度乐观/极度看多" - }, - visualization_color="#ef4444" # 红色表示负面,绿色表示正面 - ), - - "confidence": ISQDimensionSpec( - name="确定性", - key="confidence", - description="信号的可信度和确定性程度", - range_type="0.0 到 1.0", - scale_factor=1.0, - examples={ - "0.0-0.3": "信息来源不可靠/传言多/逻辑推导牵强", - "0.3-0.6": "信息相对可靠/有一定逻辑/但仍有不确定性", - "0.6-0.8": "信息来源权威/逻辑清晰/高度可信", - "0.8-1.0": "官方确认/数据明确/完全确定" - }, - visualization_color="#3b82f6" # 蓝色 - ), - - "intensity": ISQDimensionSpec( - name="强度/影响量级", - key="intensity", - description="信号对相关板块/个股的潜在影响程度", - range_type="1 到 5", - scale_factor=20.0, # 用于雷达图缩放 (5 -> 100) - examples={ - "1": "影响微弱,可能被市场忽略", - "2": "小幅影响,短期可能有波动", - "3": "中等影响,值得重点关注", - "4": "强烈影响,可能成为市场焦点", - "5": "极强影响,市场预期明显变化" - }, - visualization_color="#f97316" # 橙色 - ), - - "expectation_gap": ISQDimensionSpec( - name="预期差", - key="expectation_gap", - description="市场预期与现实之间的差距", - range_type="0.0 到 1.0", - scale_factor=1.0, - examples={ - "0.0-0.2": "市场充分认知,预期差小", - "0.2-0.5": "市场部分认知,存在一定预期差", - "0.5-0.8": "市场认知不足,预期差较大,存在博弈空间", - "0.8-1.0": "市场严重低估/高估,巨大预期差" - }, - visualization_color="#22c55e" # 绿色 - ), - - "timeliness": ISQDimensionSpec( - name="时效性", - key="timeliness", - description="信号的时间窗口紧迫度", - range_type="0.0 到 1.0", - scale_factor=1.0, - examples={ - "0.0-0.2": "长期信号,反应窗口 > 3 月", - "0.2-0.5": "中期信号,反应窗口 1-3 月", - "0.5-0.8": "短期信号,反应窗口 1 周 - 1 月", - "0.8-1.0": "超短期信号,反应窗口 < 1 周(需立即行动)" - }, - visualization_color="#a855f7" # 紫色 - ), - }, - - scoring_guide=""" - ### ISQ 评分指导 (Investment Signal Quality) - - ISQ 框架用于多维度评估投资信号的质量。每个信号由 5 个维度组成: - - 1. **情绪 (Sentiment)**: -1.0 到 1.0,表示看空(-)/中性(0)/看多(+) - 2. **确定性 (Confidence)**: 0.0 到 1.0,数值越高越确定 - 3. **强度 (Intensity)**: 1 到 5,数值越高影响越大 - 4. **预期差 (Expectation Gap)**: 0.0 到 1.0,市场预期与现实的差距 - 5. **时效性 (Timeliness)**: 0.0 到 1.0,反应窗口的紧迫程度 - - ### 综合评分算法 - - 综合评分 = 确定性 × 0.35 + 强度/5 × 0.30 + 预期差 × 0.20 + 时效性 × 0.15 - - 范围: 0.0 到 1.0 - - 0.0-0.3: 信号质量较差,不建议跟进 - - 0.3-0.6: 信号质量一般,可作参考 - - 0.6-0.8: 信号质量良好,值得跟进 - - 0.8-1.0: 信号质量优异,强烈推荐 - - ### 评分时的注意事项 - - - **不要混淆方向和强度**:情绪可以是看空,但确定性和强度仍可能很高 - - **预期差往往是 Alpha 来源**:高预期差 + 高确定性 = 最佳博弈机会 - - **考虑时间成本**:长期信号需要更高的确定性才值得跟进 - - **数据为王**:所有评分必须有具体数据支撑 - """, - - applicable_scenarios=[ - "上市公司基本面变化分析", - "产业政策与监管事件评估", - "地缘政治与宏观经济影响", - "技术进步与产业升级", - "突发事件与应急响应" - ], - - aggregation_method="weighted_average", - dimension_weights={ - "confidence": 0.35, - "intensity": 0.30, - "expectation_gap": 0.20, - "timeliness": 0.15 - } -) - - -# ===================================================== -# ISQ Template 管理系统 -# ===================================================== - -class ISQTemplateManager: - """ISQ Template 管理器""" - - def __init__(self): - self.templates: Dict[str, ISQTemplate] = { - DEFAULT_ISQ_TEMPLATE.template_id: DEFAULT_ISQ_TEMPLATE - } - - def register_template(self, template: ISQTemplate) -> None: - """注册新的 template""" - self.templates[template.template_id] = template - - def register_template_dict(self, template_dict: Dict[str, Any]) -> ISQTemplate: - """从 dict 注册模板,返回实例。""" - tpl = ISQTemplate(**template_dict) - self.register_template(tpl) - return tpl - - def get_template(self, template_id: str) -> ISQTemplate: - """获取指定 template""" - if template_id not in self.templates: - return DEFAULT_ISQ_TEMPLATE - return self.templates[template_id] - - def list_templates(self) -> List[Dict[str, str]]: - """列出所有可用 template""" - return [ - { - "id": t.template_id, - "name": t.template_name, - "description": t.description, - "dimensions": list(t.dimensions.keys()) - } - for t in self.templates.values() - ] - - def get_dimension(self, template_id: str, dimension_key: str) -> ISQDimensionSpec: - """获取指定 template 的某个维度定义""" - template = self.get_template(template_id) - return template.dimensions.get(dimension_key) - - def get_scoring_prompt(self, template_id: str) -> str: - """获取用于 LLM 的评分 prompt""" - template = self.get_template(template_id) - - dimensions_desc = "\n".join([ - f"- **{d.name} ({d.key})**\n" - f" 范围: {d.range_type}\n" - f" 说明: {d.description}\n" - f" 示例: {', '.join(f'{k}={v}' for k, v in list(d.examples.items())[:3])}" - for d in template.dimensions.values() - ]) - - return f""" -### ISQ 评估指导 ({template.template_name}) - -使用以下 {len(template.dimensions)} 个维度评估信号质量: - -{dimensions_desc} - -### 评分标准 -{template.scoring_guide} - -### 输出格式 (JSON) -请输出以下 JSON 格式的评分结果: -{{ - "sentiment": , - "confidence": , - "intensity": , - "expectation_gap": , - "timeliness": , - "rationale": {{ - "sentiment": "评分理由", - "confidence": "评分理由", - "intensity": "评分理由", - "expectation_gap": "评分理由", - "timeliness": "评分理由" - }} -}} -""" - - -# 全局 template 管理器实例 -isq_template_manager = ISQTemplateManager() - - -# ===================================================== -# 配置加载 -# ===================================================== - -def load_templates_from_config(config_path: Optional[str] = None) -> None: - """从配置目录加载所有 JSON 模板文件,未找到则跳过,不影响默认模板。 - 支持单个 JSON 文件或目录(目录下的所有 .json 文件)。 - """ - if config_path: - path = Path(config_path) - else: - # 默认目录:config/isq_templates/ - # __file__ = src/schema/isq_template.py - # parent = src/schema, parent.parent = src, parent.parent.parent = 项目根目录 - path = Path(__file__).resolve().parent.parent.parent / "config" - - if not path.exists(): - return - - # 如果是目录,扫描所有 .json 文件 - if path.is_dir(): - json_files = list(path.glob("*.json")) - else: - json_files = [path] - - for json_file in json_files: - try: - data = json.loads(json_file.read_text(encoding="utf-8")) - - # 如果是单个模板对象,转为列表 - if isinstance(data, dict): - templates = [data] - elif isinstance(data, list): - templates = data - else: - continue - - # 注册所有模板 - for tpl_dict in templates: - if not isinstance(tpl_dict, dict): - continue - try: - isq_template_manager.register_template_dict(tpl_dict) - except Exception: - # 忽略单个模板的加载错误,继续其他模板 - continue - except Exception: - # JSON 解析失败,跳过该文件 - continue - - -# 在模块加载时自动尝试加载配置模板 -load_templates_from_config() - - -# ===================================================== -# 便利函数 -# ===================================================== - -def get_isq_template(template_id: str = "default_isq_v1") -> ISQTemplate: - """获取 ISQ template""" - return isq_template_manager.get_template(template_id) - - -def get_isq_scoring_prompt(template_id: str = "default_isq_v1") -> str: - """获取用于 LLM 的 ISQ 评分 prompt""" - return isq_template_manager.get_scoring_prompt(template_id) - - -def calculate_isq_overall_score(scores: Dict[str, float], template_id: str = "default_isq_v1") -> float: - """计算 ISQ 综合评分""" - template = get_isq_template(template_id) - - overall = 0.0 - for dim_key, weight in template.dimension_weights.items(): - if dim_key in scores: - score = scores[dim_key] - # 处理强度维度的特殊缩放 (1-5 -> 0-1) - if dim_key == "intensity": - score = score / 5.0 - overall += score * weight - - return min(1.0, max(0.0, overall)) # 限制在 0-1 之间 diff --git a/skills/alphaear-reporter/scripts/schema/models.py b/skills/alphaear-reporter/scripts/schema/models.py deleted file mode 100644 index 422ca9c..0000000 --- a/skills/alphaear-reporter/scripts/schema/models.py +++ /dev/null @@ -1,100 +0,0 @@ -from pydantic import BaseModel, Field -from typing import List, Optional, Dict, Any -from datetime import datetime - -class TransmissionNode(BaseModel): - node_name: str = Field(..., description="产业链节点名称") - impact_type: str = Field(..., description="利好/利空/中性") - logic: str = Field(..., description="该节点的传导逻辑") - -class IntentAnalysis(BaseModel): - keywords: List[str] = Field(..., description="核心实体、事件或概念关键词") - search_queries: List[str] = Field(..., description="优化后的搜索引擎查询词") - is_specific_event: bool = Field(..., description="是否查询特定突发事件") - time_range: str = Field(..., description="时间范围 (recent/all/specific_date)") - intent_summary: str = Field(..., description="一句话意图描述") - -class FilterResult(BaseModel): - """LLM 筛选结果 - 快速判断是否有有效信号""" - has_valid_signals: bool = Field(..., description="列表中是否包含有效的金融信号") - selected_ids: List[int] = Field(default_factory=list, description="筛选出的有效信号 ID 列表") - themes: List[str] = Field(default_factory=list, description="信号涉及的主题") - reason: Optional[str] = Field(default=None, description="如果无有效信号,说明原因") - -class InvestmentSignal(BaseModel): - # 核心元数据 - signal_id: str = Field(default="unknown_sig", description="唯一信号 ID") - title: str = Field(..., description="信号标题") - summary: str = Field(default="暂无摘要分析", description="100 字核心观点快报") - reasoning: str = Field(default="", description="详细的推演逻辑和理由") - - # 逻辑传导 (ISQ Key 1) - transmission_chain: List[TransmissionNode] = Field(default_factory=list, description="产业链传导逻辑链条") - - # 信号质量 (ISQ Key 2) - 来自 isq_template.DEFAULT_ISQ_TEMPLATE - # 参考: src/schema/isq_template.py 的 DEFAULT_ISQ_TEMPLATE 定义 - sentiment_score: float = Field(default=0.0, description="[ISQ] 情绪/走势 (-1.0=极度看空 ~ 0.0=中性 ~ 1.0=极度看多)") - confidence: float = Field(default=0.5, description="[ISQ] 确定性 (0.0=不可信 ~ 1.0=完全确定)") - intensity: int = Field(default=3, description="[ISQ] 强度/影响量级 (1=微弱 ~ 5=极强)") - expectation_gap: float = Field(default=0.5, description="[ISQ] 预期差/博弈空间 (0.0=充分定价 ~ 1.0=巨大预期差)") - timeliness: float = Field(default=0.8, description="[ISQ] 时效性 (0.0=长期 ~ 1.0=超短期)") - - # 预测与博弈 (ISQ Key 3) - expected_horizon: str = Field(default="T+N", description="预期的反应时窗 (如: T+0, T+3, Long-term)") - price_in_status: str = Field(default="未知", description="市场预期消化程度 (未定价/部分定价/充分定价)") - - # 关联实体 - impact_tickers: List[Dict[str, Any]] = Field(default_factory=list, description="受影响的代码列表及其权重") - industry_tags: List[str] = Field(default_factory=list, description="关联行业标签") - - # 溯源 - sources: List[Dict[str, str]] = Field(default_factory=list, description="来源详情 (包含 title, url, source_name)") - -class ResearchContext(BaseModel): - """研究员搜集的背景信息结构""" - raw_signal: str = Field(..., description="原始信号内容") - tickers_found: List[Dict[str, Any]] = Field(default_factory=list, description="找到的相关标的及其基本面/股价信息") - industry_background: str = Field(..., description="行业背景及产业链现状") - latest_developments: List[str] = Field(default_factory=list, description="相关事件的最新进展") - key_risks: List[str] = Field(default_factory=list, description="潜在风险点") - search_results_summary: str = Field(..., description="搜索结果的综合摘要") - -class ScanContext(BaseModel): - """扫描员搜集的原始数据结构""" - hot_topics: List[str] = Field(..., description="当前市场热点话题") - news_summaries: List[Dict[str, Any]] = Field(..., description="关键新闻摘要列表") - market_data: Dict[str, Any] = Field(default_factory=dict, description="相关的市场行情数据") - sentiment_overview: str = Field(..., description="整体市场情绪概览") - raw_data_summary: str = Field(..., description="原始数据的综合摘要") - -class SignalCluster(BaseModel): - theme_title: str = Field(..., description="主题名称") - signal_ids: List[int] = Field(..., description="包含的信号 ID 列表") - rationale: str = Field(..., description="聚类理由") - -class ClusterContext(BaseModel): - """信号聚类结果结构""" - clusters: List[SignalCluster] = Field(..., description="聚类列表") - -class KLinePoint(BaseModel): - date: str = Field(..., description="日期") - open: float = Field(..., description="开盘价") - high: float = Field(..., description="最高价") - low: float = Field(..., description="最低价") - close: float = Field(..., description="收盘价") - volume: float = Field(..., description="成交量") - -class ForecastResult(BaseModel): - ticker: str = Field(..., description="股票代码") - base_forecast: List[KLinePoint] = Field(default_factory=list, description="Kronos 模型原始预测") - adjusted_forecast: List[KLinePoint] = Field(default_factory=list, description="LLM 调整后的预测") - rationale: str = Field(default="", description="预测调整理由及逻辑说明") - timestamp: str = Field(default_factory=lambda: datetime.now().strftime("%Y-%m-%d %H:%M:%S"), description="生成时间") - -class InvestmentReport(BaseModel): - overall_sentiment: str = Field(..., description="整体市场情绪评价") - market_entropy: float = Field(..., description="市场分歧度 (0-1, 1代表极高分歧)") - signals: List[InvestmentSignal] = Field(..., description="深度解析的投资信号列表") - forecasts: List[ForecastResult] = Field(default_factory=list, description="相关标的的预测结果") - timestamp: str = Field(..., description="报告生成时间") - meta_info: Optional[Dict[str, Any]] = Field(default_factory=dict, description="其他元数据") diff --git a/skills/alphaear-reporter/scripts/tools/__init__.py b/skills/alphaear-reporter/scripts/tools/__init__.py deleted file mode 100644 index 97fbb5d..0000000 --- a/skills/alphaear-reporter/scripts/tools/__init__.py +++ /dev/null @@ -1,24 +0,0 @@ -# src/tools/__init__.py -""" -AlphaEar 工具包层 - Agno Toolkit 适配器 - -提供的 Toolkit 类: -- NewsToolkit: 热点新闻获取 -- StockToolkit: 股票搜索与价格查询 -- SentimentToolkit: 情绪分析 -- SearchToolkit: 网络搜索 -""" - -from .toolkits import ( - NewsToolkit, - StockToolkit, - SentimentToolkit, - SearchToolkit, -) - -__all__ = [ - "NewsToolkit", - "StockToolkit", - "SentimentToolkit", - "SearchToolkit", -] diff --git a/skills/alphaear-reporter/scripts/tools/toolkits.py b/skills/alphaear-reporter/scripts/tools/toolkits.py deleted file mode 100644 index ebd0b69..0000000 --- a/skills/alphaear-reporter/scripts/tools/toolkits.py +++ /dev/null @@ -1,526 +0,0 @@ -""" -AlphaEar 工具包层 - Agno Toolkit 适配器 -复用 utils 中的底层工具实现,提供 Agno Agent 兼容的 Toolkit 接口 -""" -from datetime import datetime -from typing import Optional -from agno.tools import Toolkit -from loguru import logger - -from ..utils.database_manager import DatabaseManager -from ..utils.news_tools import NewsNowTools, PolymarketTools -from ..utils.stock_tools import StockTools -from ..utils.search_tools import SearchTools -from ..utils.sentiment_tools import SentimentTools - - -class NewsToolkit(Toolkit): - """ - 新闻工具包 - 包装 NewsNowTools 为 Agno Toolkit - - 提供热点新闻获取、内容提取等功能 - """ - - def __init__(self, db: DatabaseManager, **kwargs): - self._news_tools = NewsNowTools(db) - self._sources = self._news_tools.SOURCES - - tools = [ - self.fetch_hot_news, - self.fetch_news_content, - self.get_unified_trends, - self.enrich_news_content, - ] - super().__init__(name="news_toolkit", tools=tools, **kwargs) - - - def fetch_hot_news(self, source_id: str, count: int = 10) -> str: - """ - 从指定新闻源获取热点新闻列表。 - - Args: - source_id: 新闻源标识符。可选值按类别: - **金融类**: "cls" (财联社), "wallstreetcn" (华尔街见闻), "xueqiu" (雪球) - **综合类**: "weibo" (微博热搜), "zhihu" (知乎热榜), "baidu" (百度热搜), - "toutiao" (今日头条), "douyin" (抖音), "thepaper" (澎湃新闻) - **科技类**: "36kr" (36氪), "ithome" (IT之家), "v2ex", "juejin" (掘金), - "hackernews" (Hacker News) - 推荐金融分析使用 "cls", "wallstreetcn", "xueqiu"。 - count: 获取的新闻数量,默认 10 条。 - - Returns: - 热点新闻列表的文本描述,包含排名、标题和链接。如果源不可用则返回错误信息。 - """ - logger.info(f"🔧 [TOOL CALLED] fetch_hot_news(source_id={source_id}, count={count})") - - items = self._news_tools.fetch_hot_news(source_id, count=count, fetch_content=False) - - if not items: - return f"获取 {source_id} 热点失败" - - source_name = self._sources.get(source_id, source_id) - result = f"## {source_name} 热点 (获取时间: {datetime.now().strftime('%Y-%m-%d %H:%M')})\n\n" - - for item in items: - result += f"{item['rank']}. {item['title']}\n 链接: {item['url']}\n\n" - - logger.info(f"✅ [TOOL SUCCESS] Got {len(items)} news items from {source_id}") - return result - - def fetch_news_content(self, url: str) -> str: - """ - 使用 Jina Reader 抓取指定 URL 的网页正文内容。 - - Args: - url: 需要抓取内容的完整网页 URL,必须以 http:// 或 https:// 开头。 - - Returns: - 提取的网页正文内容,如果失败则返回错误信息。 - """ - content = self._news_tools.fetch_news_content(url) - if content: - return content[:5000] # 限制长度 - return "内容抓取失败" - - def get_unified_trends(self, sources: str = "wallstreetcn,cls") -> str: - """ - 获取多平台综合热点报告。 - - Args: - sources: 要扫描的新闻源,用逗号分隔。 - 可选值: weibo, zhihu, baidu, toutiao, wallstreetcn, cls - 默认: "wallstreetcn,cls" (金融资讯) - - Returns: - 格式化的热点汇总报告。 - """ - source_list = [s.strip() for s in sources.split(",")] - report = self._news_tools.get_unified_trends(source_list) - return report - - def enrich_news_content(self, source: str = None, limit: int = 5) -> str: - """ - 为数据库中缺少正文内容的新闻补充内容。 - - Args: - source: 筛选特定新闻源(如 "cls"),为空则处理所有。 - limit: 最多处理的新闻数量,默认 5 条。 - - Returns: - 处理结果的描述。 - """ - logger.info(f"🔧 [TOOL CALLED] enrich_news_content(source={source}, limit={limit})") - - # 获取需要补充内容的新闻 - news_items = self._news_tools.db.get_daily_news(source=source, limit=limit) - items_without_content = [n for n in news_items if not n.get('content')] - - if not items_without_content: - return "没有需要补充内容的新闻" - - updated_count = 0 - cursor = self._news_tools.db.conn.cursor() - - for item in items_without_content[:limit]: - url = item.get('url') - if url: - content = self._news_tools.fetch_news_content(url) - if content: - cursor.execute( - "UPDATE daily_news SET content = ? WHERE id = ?", - (content[:10000], item['id']) - ) - updated_count += 1 - - self._news_tools.db.conn.commit() - logger.info(f"✅ [TOOL SUCCESS] Enriched {updated_count} news items with content") - - return f"✅ 已为 {updated_count} 条新闻补充正文内容" - - -class PolymarketToolkit(Toolkit): - """ - Polymarket 预测市场工具包 - 获取热门预测市场数据 - - 预测市场数据可反映公众情绪、预期和关注度 - """ - - def __init__(self, db: DatabaseManager, **kwargs): - self._poly_tools = PolymarketTools(db) - - tools = [ - self.get_prediction_markets, - self.get_market_summary, - ] - super().__init__(name="polymarket_toolkit", tools=tools, **kwargs) - - def get_prediction_markets(self, limit: int = 20) -> str: - """ - 获取 Polymarket 活跃预测市场的关键数据。 - - 预测市场反映公众对重大事件的概率预期,可用于: - - 分析市场情绪和风险偏好 - - 了解热门话题的关注度 - - 获取重大事件的概率预期 - - Args: - limit: 获取的市场数量,默认 20 个。 - - Returns: - 预测市场数据列表,包含问题、结果概率和交易量。 - 如果获取失败返回错误信息。 - """ - logger.info(f"🔧 [TOOL CALLED] get_prediction_markets(limit={limit})") - - markets = self._poly_tools.get_active_markets(limit) - if not markets: - return "❌ 无法获取 Polymarket 数据(可能是网络问题)" - - result = f"## 🔮 Polymarket 热门预测 (共 {len(markets)} 个)\n\n" - for i, m in enumerate(markets[:limit], 1): - question = m.get("question", "Unknown") - prices = m.get("outcomePrices", []) - volume = m.get("volume", 0) - - result += f"{i}. **{question}**\n" - if prices: - result += f" 概率: {prices}\n" - if volume: - try: - result += f" 交易量: ${float(volume):,.0f}\n" - except: - result += f" 交易量: {volume}\n" - result += "\n" - - logger.info(f"✅ [TOOL SUCCESS] Got {len(markets)} prediction markets") - return result - - def get_market_summary(self, limit: int = 10) -> str: - """ - 获取预测市场摘要报告,了解当前热门话题和公众预期。 - - Args: - limit: 获取的市场数量,默认 10 个。 - - Returns: - 格式化的预测市场报告。 - """ - return self._poly_tools.get_market_summary(limit) - - -class StockToolkit(Toolkit): - - """ - 股票工具包 - 包装 StockTools 为 Agno Toolkit - - 提供股票搜索、价格查询等功能 - """ - - def __init__(self, db: DatabaseManager, **kwargs): - self._stock_tools = StockTools(db) - - tools = [ - self.search_ticker, - self.get_stock_price, - ] - super().__init__(name="stock_toolkit", tools=tools, **kwargs) - - def search_ticker(self, query: str) -> str: - """ - 模糊搜索 A 股股票代码或名称。 - - Args: - query: 搜索关键词,可以是股票代码(如 "600519")或名称关键词(如 "茅台"、"宁德"、"比亚迪")。 - - Returns: - 匹配的股票列表,包含代码和名称。 - """ - q = (query or "").strip() - # Guardrails: prevent overly generic queries that tend to return arbitrary "...股份" matches. - generic_terms = { - "股份", - "有限公司", - "概念股", - "受益股", - "龙头", - "标的", - "相关股票", - "合作概念股", - } - if not q: - return "查询为空,无法搜索股票" - if q in generic_terms: - return f"查询过于泛化({q}),为避免误匹配已拒绝。请提供更具体的公司名或6位代码。" - # If it's not a numeric code, require at least 2 non-space chars. - if not any(ch.isdigit() for ch in q) and len(q.replace(" ", "")) < 2: - return "查询过短,无法搜索股票。请提供更具体的公司名或6位代码。" - - results = self._stock_tools.search_ticker(query) - - if not results: - return f"未找到匹配 '{query}' 的股票" - - output = f"## 股票搜索结果 (关键词: {query})\n\n" - for r in results: - output += f"- {r['code']} - {r['name']}\n" - return output - - def get_stock_price(self, ticker: str, days: int = 30) -> str: - """ - 获取指定股票的近期价格走势。 - - Args: - ticker: 股票代码,如 "600519"(贵州茅台)或 "000001"(平安银行)。 - days: 查询天数,默认 30 天。 - - Returns: - 价格走势的文本摘要。 - """ - from datetime import timedelta - end_date = datetime.now().strftime('%Y-%m-%d') - start_date = (datetime.now() - timedelta(days=days)).strftime('%Y-%m-%d') - - df = self._stock_tools.get_stock_price(ticker, start_date, end_date) - - if df.empty: - return f"未能获取 {ticker} 的股价数据" - - - latest = df.iloc[-1] - change = ((latest['close'] - df.iloc[0]['close']) / df.iloc[0]['close']) * 100 - - # 格式化历史数据供 LLM 分析 (取最近 15 天) - history_df = df.tail(15).copy() - history_df['date'] = history_df['date'].astype(str) - # 简化列名以节省 token - history_cols = ['date', 'open', 'close', 'high', 'low', 'volume'] - - # 尝试使用 markdown 格式,如果失败退回到 string - try: - history_str = history_df[history_cols].to_markdown(index=False, numalign="left", stralign="left") - except ImportError: - history_str = history_df[history_cols].to_string(index=False) - except Exception: - history_str = history_df[history_cols].to_string(index=False) - - return f"""## {ticker} 价格走势 ({days}天) -- 当前价: ¥{latest['close']:.2f} -- 期间涨跌: {change:+.2f}% -- 最高/最低: ¥{df['high'].max():.2f} / ¥{df['low'].min():.2f} -- 数据范围: {df.iloc[0]['date']} -> {latest['date']} - -### 最近 15 个交易日详细数据 (OHLCV): -{history_str} -""" - - - -class SentimentToolkit(Toolkit): - """ - 情绪分析工具包 - 包装 SentimentTools 为 Agno Toolkit - - 提供文本情绪分析功能(支持 BERT 和 LLM 模式) - """ - - def __init__(self, db: DatabaseManager, mode: str = "auto", **kwargs): - self._sentiment_tools = SentimentTools(db, mode=mode) - self._db = db - - tools = [ - self.analyze_sentiment, - self.batch_update_sentiment, - ] - super().__init__(name="sentiment_toolkit", tools=tools, **kwargs) - - def analyze_sentiment(self, text: str) -> str: - """ - 分析文本的情绪极性。 - - Args: - text: 需要分析的文本内容,如新闻标题或摘要。 - - Returns: - 情绪分析结果,包含分值(-1.0到1.0)和标签(positive/negative/neutral)。 - """ - result = self._sentiment_tools.analyze_sentiment(text) - - score = result.get('score', 0.0) - label = result.get('label', 'neutral') - reason = result.get('reason', '') - - return f"""情绪分析结果: -- 文本: {text[:100]}{'...' if len(text) > 100 else ''} -- 分值: {score:.2f} -- 标签: {label} -- 分析: {reason}""" - - def batch_update_sentiment(self, source: str = None, limit: int = 20) -> str: - """ - 批量更新数据库中新闻的情绪分数。 - - Args: - source: 筛选特定新闻源(如 "cls", "wallstreetcn"),为空则处理所有。 - limit: 最多处理的新闻数量,默认 20 条。 - - Returns: - 更新结果的描述。 - """ - logger.info(f"🔧 [TOOL CALLED] batch_update_sentiment(source={source}, limit={limit})") - - count = self._sentiment_tools.batch_update_news_sentiment(source=source, limit=limit) - - return f"✅ 已更新 {count} 条新闻的情绪分数" - - - -class SearchToolkit(Toolkit): - """ - 搜索工具包 - 包装 SearchTools 为 Agno Toolkit - - 提供网络搜索功能(支持 Jina、DuckDuckGo 和百度) - - 当环境变量 JINA_API_KEY 设置时,默认使用 Jina Search, - 提供 LLM 友好的搜索结果。 - """ - - def __init__(self, db: DatabaseManager, **kwargs): - self._search_tools = SearchTools(db) - - tools = [ - self.web_search, - self.aggregate_search, - ] - super().__init__(name="search_toolkit", tools=tools, **kwargs) - - def web_search(self, query: str, engine: str = None, max_results: int = 5) -> str: - """ - 使用指定搜索引擎执行网络搜索。 - - Args: - query: 搜索关键词,如 "英伟达财报" 或 "光伏行业政策"。 - engine: 搜索引擎选择。可选值: - "jina" (Jina Search,需配置 JINA_API_KEY,LLM友好输出), - "ddg" (DuckDuckGo,推荐英文/国际搜索), - "baidu" (百度,推荐中文/国内搜索)。 - 默认: 若配置了 JINA_API_KEY 则使用 "jina",否则 "ddg"。 - max_results: 返回结果数量。默认 5。 - - Returns: - 搜索结果的文本描述。 - """ - return self._search_tools.search(query, engine=engine, max_results=max_results) - - def aggregate_search(self, query: str, max_results: int = 5) -> str: - """ - 同时使用多个搜索引擎搜索并聚合结果。 - - Args: - query: 搜索关键词。 - max_results: 每个引擎返回的最大结果数。默认 5。 - - Returns: - 聚合后的搜索结果。 - """ - return self._search_tools.aggregate_search(query, max_results=max_results) - - -class ContextSearchToolkit(Toolkit): - """ - 上下文搜索工具包 - 用于 RAG 场景的文档片段检索 - - 支持在内存中存储文档片段,并通过关键词搜索相关内容。 - 适用于 ReportAgent 的分段编辑场景。 - """ - - def __init__(self, **kwargs): - self._store = {} # {doc_id: {"title": str, "content": str, "summary": str}} - - tools = [ - self.search_context, - self.get_toc, - ] - super().__init__(name="context_search_toolkit", tools=tools, **kwargs) - - def add_document(self, doc_id: str, title: str, content: str, summary: str = ""): - """添加文档到存储(供外部调用,非 LLM 工具)""" - self._store[doc_id] = { - "title": title, - "content": content, - "summary": summary or content[:200] + "..." - } - logger.info(f"📄 Added document to context store: {doc_id} - {title[:30]}...") - - def clear(self): - """清空文档存储""" - self._store.clear() - logger.info("🗑️ Context store cleared") - - def search_context(self, query: str, max_results: int = 3) -> str: - """ - 在已存储的文档中搜索与查询相关的内容片段。 - - Args: - query: 搜索关键词,如 "消费板块" 或 "茅台 预测"。 - max_results: 返回的最大结果数,默认 3。 - - Returns: - 匹配的文档片段,按相关性排序。 - """ - logger.info(f"🔍 [TOOL CALLED] search_context(query={query}, max_results={max_results})") - - if not self._store: - return "⚠️ 上下文存储为空,无可搜索内容。" - - # 简单的关键词匹配 + 计分 - query_terms = query.lower().split() - results = [] - - for doc_id, doc in self._store.items(): - score = 0 - content_lower = doc["content"].lower() - title_lower = doc["title"].lower() - - for term in query_terms: - # 标题匹配权重更高 - if term in title_lower: - score += 3 - if term in content_lower: - score += content_lower.count(term) - - if score > 0: - results.append((score, doc_id, doc)) - - # 按分数排序 - results.sort(key=lambda x: x[0], reverse=True) - results = results[:max_results] - - if not results: - return f"未找到与 '{query}' 相关的内容。" - - output = f"## 搜索结果 (查询: {query})\n\n" - for score, doc_id, doc in results: - output += f"### [{doc_id}] {doc['title']}\n" - # 返回摘要而非全文,节省 token - output += f"{doc['summary']}\n\n" - - logger.info(f"✅ [TOOL SUCCESS] Found {len(results)} matching documents") - return output - - def get_toc(self) -> str: - """ - 获取当前存储的所有文档的目录(TOC)。 - - Returns: - 文档目录列表,包含 ID 和标题。 - """ - logger.info("🔍 [TOOL CALLED] get_toc()") - - if not self._store: - return "⚠️ 上下文存储为空。" - - output = "## 文档目录 (TOC)\n\n" - for doc_id, doc in self._store.items(): - output += f"- **[{doc_id}]** {doc['title']}\n" - - return output - diff --git a/skills/alphaear-reporter/scripts/utils/__init__.py b/skills/alphaear-reporter/scripts/utils/__init__.py deleted file mode 100644 index 27e1961..0000000 --- a/skills/alphaear-reporter/scripts/utils/__init__.py +++ /dev/null @@ -1 +0,0 @@ -# AlphaEar utils package diff --git a/skills/alphaear-reporter/scripts/utils/content_extractor.py b/skills/alphaear-reporter/scripts/utils/content_extractor.py deleted file mode 100644 index 133207a..0000000 --- a/skills/alphaear-reporter/scripts/utils/content_extractor.py +++ /dev/null @@ -1,122 +0,0 @@ -import requests -from requests.exceptions import RequestException, Timeout, ConnectionError -import os -import time -import json -import threading -from typing import Optional -from loguru import logger - - -class ContentExtractor: - """内容提取工具 - 主要接入 Jina Reader API""" - - JINA_BASE_URL = "https://r.jina.ai/" - - # 速率限制配置 (无 API Key 时:20 次/分钟) - _rate_limit_no_key = 20 # 每分钟最大请求数 - _rate_window = 60.0 # 时间窗口(秒) - _min_interval = 3.0 # 请求最小间隔(秒) - - # 类级别的速率限制状态 - _request_times = [] - _last_request_time = 0.0 - _lock = threading.Lock() - - @classmethod - def _wait_for_rate_limit(cls, has_api_key: bool) -> None: - """等待以满足速率限制要求""" - if has_api_key: - # 有 API Key 时,只需保持最小间隔 - time.sleep(0.5) - return - - with cls._lock: - current_time = time.time() - - # 1. 清理过期的请求记录 - cls._request_times = [t for t in cls._request_times if current_time - t < cls._rate_window] - - # 2. 检查是否达到速率限制 - if len(cls._request_times) >= cls._rate_limit_no_key: - # 需要等待最旧的请求过期 - oldest = cls._request_times[0] - wait_time = cls._rate_window - (current_time - oldest) + 1.0 - if wait_time > 0: - logger.warning(f"⏳ Jina rate limit reached, waiting {wait_time:.1f}s...") - time.sleep(wait_time) - current_time = time.time() - cls._request_times = [t for t in cls._request_times if current_time - t < cls._rate_window] - - # 3. 确保请求间隔不太快 - time_since_last = current_time - cls._last_request_time - if time_since_last < cls._min_interval: - sleep_time = cls._min_interval - time_since_last - time.sleep(sleep_time) - - # 4. 记录本次请求 - cls._request_times.append(time.time()) - cls._last_request_time = time.time() - - @classmethod - def extract_with_jina(cls, url: str, timeout: int = 30) -> Optional[str]: - """ - 使用 Jina Reader 提取网页正文内容 (Markdown 格式) - - 无 API Key 时自动限速:每分钟最多 20 次请求,每次间隔至少 3 秒 - """ - if not url or not url.startswith("http"): - return None - - logger.info(f"🕸️ Extracting content from: {url} via Jina...") - - headers = { - "User-Agent": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/131.0.0.0 Safari/537.36", - "Accept": "application/json" - } - - # 使用统一的 JINA_API_KEY - api_key = os.getenv("JINA_API_KEY") - has_api_key = bool(api_key and api_key.strip()) - - if has_api_key: - headers["Authorization"] = f"Bearer {api_key}" - - # 等待速率限制 - cls._wait_for_rate_limit(has_api_key) - - try: - # Jina Reader API - full_url = f"{cls.JINA_BASE_URL}{url}" - response = requests.get(full_url, headers=headers, timeout=timeout) - - if response.status_code == 200: - try: - data = response.json() - # Jina JSON 响应格式通常在 data.content - if isinstance(data, dict) and "data" in data: - return data["data"].get("content", "") - return data.get("content", response.text) - except (json.JSONDecodeError, TypeError): - return response.text - elif response.status_code == 429: - # 触发速率限制,等待后重试一次 - logger.warning(f"⚠️ Jina rate limit (429), waiting 60s before retry...") - time.sleep(60) - return cls.extract_with_jina(url, timeout) - else: - logger.warning(f"Jina extraction failed (Status {response.status_code}) for {url}") - return None - - except Timeout: - logger.error(f"Timeout during Jina extraction for {url}") - return None - except ConnectionError: - logger.error(f"Connection error during Jina extraction for {url}") - return None - except RequestException as e: - logger.error(f"Request error during Jina extraction: {e}") - return None - except Exception as e: - logger.error(f"Unexpected error during Jina extraction: {e}") - return None diff --git a/skills/alphaear-reporter/scripts/utils/database_manager.py b/skills/alphaear-reporter/scripts/utils/database_manager.py deleted file mode 100644 index cfc362b..0000000 --- a/skills/alphaear-reporter/scripts/utils/database_manager.py +++ /dev/null @@ -1,581 +0,0 @@ -import sqlite3 -import json -from datetime import datetime, date -from pathlib import Path -from typing import List, Dict, Optional, Any, Union -import pandas as pd -from loguru import logger - -class DatabaseManager: - """ - AlphaEar 数据库管理器 - 负责存储热点数据、搜索缓存和股价数据 - 使用 SQLite 进行持久化存储 - """ - - def __init__(self, db_path: str = "data/signal_flux.db"): - self.db_path = Path(db_path) - self.db_path.parent.mkdir(parents=True, exist_ok=True) - self.conn = sqlite3.connect(str(self.db_path), check_same_thread=False) - self.conn.row_factory = sqlite3.Row - self._init_db() - logger.info(f"💾 Database initialized at {self.db_path}") - - def _init_db(self): - """初始化表结构""" - cursor = self.conn.cursor() - - # 1. 每日热点新闻表 - cursor.execute(""" - CREATE TABLE IF NOT EXISTS daily_news ( - id TEXT PRIMARY KEY, - source TEXT, - rank INTEGER, - title TEXT, - url TEXT, - content TEXT, - publish_time TEXT, - crawl_time TEXT, - sentiment_score REAL, - analysis TEXT, - meta_data TEXT - ) - """) - - # 尝试添加 analysis 列(如果表已存在但没有该列) - try: - cursor.execute("ALTER TABLE daily_news ADD COLUMN analysis TEXT") - except: - pass # 列已存在 - - - # 2. 搜索缓存表 (原有 JSON 缓存) - cursor.execute(""" - CREATE TABLE IF NOT EXISTS search_cache ( - query_hash TEXT PRIMARY KEY, - query TEXT, - engine TEXT, - results TEXT, - timestamp TEXT - ) - """) - - # 2.5 搜索详情表 (展开的搜索结果) - cursor.execute(""" - CREATE TABLE IF NOT EXISTS search_detail ( - id TEXT, - query_hash TEXT, - rank INTEGER, - title TEXT, - url TEXT, - content TEXT, - publish_time TEXT, - crawl_time TEXT, - sentiment_score REAL, - source TEXT, - meta_data TEXT, - PRIMARY KEY (query_hash, id) - ) - """) - - # 3. 股价数据表 - cursor.execute(""" - CREATE TABLE IF NOT EXISTS stock_prices ( - ticker TEXT, - date TEXT, - open REAL, - close REAL, - high REAL, - low REAL, - volume REAL, - change_pct REAL, - PRIMARY KEY (ticker, date) - ) - """) - - # 4. 股票列表表 (用于检索) - cursor.execute(""" - CREATE TABLE IF NOT EXISTS stock_list ( - code TEXT PRIMARY KEY, - name TEXT - ) - """) - - # 5. 投资信号表 (ISQ Framework) - cursor.execute(""" - CREATE TABLE IF NOT EXISTS signals ( - signal_id TEXT PRIMARY KEY, - title TEXT, - summary TEXT, - transmission_chain TEXT, - sentiment_score REAL, - confidence REAL, - intensity INTEGER, - expected_horizon TEXT, - price_in_status TEXT, - impact_tickers TEXT, - industry_tags TEXT, - sources TEXT, - user_id TEXT, - created_at TEXT - ) - """) - - - - # 6. 创建索引以优化查询性能 - cursor.execute("CREATE INDEX IF NOT EXISTS idx_news_crawl_time ON daily_news(crawl_time)") - cursor.execute("CREATE INDEX IF NOT EXISTS idx_news_source ON daily_news(source)") - cursor.execute("CREATE INDEX IF NOT EXISTS idx_search_cache_timestamp ON search_cache(timestamp)") - cursor.execute("CREATE INDEX IF NOT EXISTS idx_stock_prices_ticker_date ON stock_prices(ticker, date)") - # 尝试添加 user_id 列到 signals 表 - try: - cursor.execute("ALTER TABLE signals ADD COLUMN user_id TEXT") - except: - pass - - cursor.execute("CREATE INDEX IF NOT EXISTS idx_signals_user_id ON signals(user_id)") - - self.conn.commit() - - # - # self.conn.commit() - - - # --- 新闻数据操作 --- - - def save_daily_news(self, news_list: List[Dict]) -> int: - """保存热点新闻,包含发布时间与抓取时间""" - cursor = self.conn.cursor() - count = 0 - crawl_time = datetime.now().isoformat() - - for news in news_list: - try: - # 兼容不同来源的 ID 生成逻辑 - news_id = news.get('id') or f"{news.get('source')}_{news.get('rank')}_{crawl_time[:10]}" - cursor.execute(""" - INSERT OR REPLACE INTO daily_news - (id, source, rank, title, url, content, publish_time, crawl_time, sentiment_score, meta_data) - VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?) - """, ( - news_id, - news.get('source'), - news.get('rank'), - news.get('title'), - news.get('url'), - news.get('content', ''), - news.get('publish_time'), # 新增支持发布时间 - crawl_time, - news.get('sentiment_score'), - json.dumps(news.get('meta_data', {})) - )) - count += 1 - except sqlite3.Error as e: - logger.error(f"Database error saving news item {news.get('title')}: {e}") - except Exception as e: - logger.error(f"Unexpected error saving news item {news.get('title')}: {e}") - - self.conn.commit() - return count - - def get_daily_news(self, source: Optional[str] = None, limit: int = 100, days: int = 1) -> List[Dict]: - """获取最近 N 天的热点新闻""" - cursor = self.conn.cursor() - # 使用 crawl_time 过滤,保证结果的新鲜度 - time_threshold = (datetime.now().timestamp() - days * 86400) - time_threshold_str = datetime.fromtimestamp(time_threshold).isoformat() - - query = "SELECT * FROM daily_news WHERE crawl_time >= ?" - params = [time_threshold_str] - - if source: - query += " AND source = ?" - params.append(source) - - query += " ORDER BY crawl_time DESC, rank LIMIT ?" - params.append(limit) - - cursor.execute(query, params) - return [dict(row) for row in cursor.fetchall()] - - def lookup_reference_by_url(self, url: str) -> Optional[Dict[str, Any]]: - """Best-effort lookup of a source item by URL. - - This is used to render a stable bibliography from DB-backed metadata. - It searches both `daily_news` and `search_detail`. - """ - url = (url or "").strip() - if not url: - return None - - cursor = self.conn.cursor() - - try: - cursor.execute( - """ - SELECT title, source, publish_time, crawl_time, url - FROM daily_news - WHERE url = ? - ORDER BY crawl_time DESC - LIMIT 1 - """, - (url,), - ) - row = cursor.fetchone() - if row: - return dict(row) - except Exception: - pass - - try: - cursor.execute( - """ - SELECT title, source, publish_time, crawl_time, url - FROM search_detail - WHERE url = ? - ORDER BY crawl_time DESC - LIMIT 1 - """, - (url,), - ) - row = cursor.fetchone() - if row: - return dict(row) - except Exception: - pass - - return None - - def delete_news(self, news_id: str) -> bool: - """删除特定新闻""" - cursor = self.conn.cursor() - cursor.execute("DELETE FROM daily_news WHERE id = ?", (news_id,)) - self.conn.commit() - return cursor.rowcount > 0 - - def update_news_content(self, news_id: str, content: str = None, analysis: str = None) -> bool: - """更新新闻的内容或分析结果""" - cursor = self.conn.cursor() - updates = [] - params = [] - - if content is not None: - updates.append("content = ?") - params.append(content) - if analysis is not None: - updates.append("analysis = ?") - params.append(analysis) - - if not updates: - return False - - params.append(news_id) - query = f"UPDATE daily_news SET {', '.join(updates)} WHERE id = ?" - cursor.execute(query, params) - self.conn.commit() - return cursor.rowcount > 0 - - # --- 搜索缓存辅助 --- - - def get_search_cache(self, query_hash: str, ttl_seconds: Optional[int] = None) -> Optional[Dict]: - """获取搜索缓存 (优先查 search_detail)""" - cursor = self.conn.cursor() - - # 1. 尝试从 search_detail 获取展开的结构化数据 - cursor.execute(""" - SELECT * FROM search_detail - WHERE query_hash = ? - ORDER BY rank - """, (query_hash,)) - details = [dict(row) for row in cursor.fetchall()] - - if details: - # 检查 TTL (取第一条的时间) - first_time = datetime.fromisoformat(details[0]['crawl_time']) - if ttl_seconds and (datetime.now() - first_time).total_seconds() > ttl_seconds: - logger.info(f"⌛ Detailed cache expired for hash {query_hash}") - pass # Expired, fall through or return None? If Detail expired, Cache likely expired too. - # But let's check basic cache just in case metadata differs? - # Actually if details exist, we prefer them. If expired, we return None. - return None - - logger.info(f"✅ Hit detailed search cache for {query_hash} ({len(details)} items)") - # Reconstruct the expected 'results' list format for SearchTools - # SearchTools expects a list of dicts. - # We return a dict wrapper to match get_search_cache signature returning Dict usually containing 'results' string. - # But SearchTools logic: - # cache = db.get_search_cache(...) - # cached_data = json.loads(cache['results']) - - # To minimize SearchTools changes, we can return a dict mimicking the old structure - # OR Change SearchTools to handle list return. - # Let's return a special dict that SearchTools can recognize or just format it as before. - return {"results": json.dumps(details), "timestamp": details[0]['crawl_time']} - - # 2. Fallback to old table - cursor.execute("SELECT * FROM search_cache WHERE query_hash = ?", (query_hash,)) - row = cursor.fetchone() - - if not row: - return None - - row_dict = dict(row) - if ttl_seconds: - cache_time = datetime.fromisoformat(row_dict['timestamp']) - if (datetime.now() - cache_time).total_seconds() > ttl_seconds: - logger.info(f"⌛ Cache expired for hash {query_hash}") - return None - - return row_dict - - def save_search_cache(self, query_hash: str, query: str, engine: str, results: Union[str, List[Dict]]): - """保存搜索结果 (同时保存到 search_cache 和 search_detail)""" - cursor = self.conn.cursor() - current_time = datetime.now().isoformat() - - results_str = results if isinstance(results, str) else json.dumps(results) - - # 1. Save summary to search_cache - cursor.execute(""" - INSERT OR REPLACE INTO search_cache (query_hash, query, engine, results, timestamp) - VALUES (?, ?, ?, ?, ?) - """, (query_hash, query, engine, results_str, current_time)) - - # 2. Save details to search_detail if results is a list - if isinstance(results, list): - for item in results: - try: - item_id = item.get('id') or f"{hash(item.get('url', ''))}" - cursor.execute(""" - INSERT OR REPLACE INTO search_detail - (id, query_hash, rank, title, url, content, publish_time, crawl_time, sentiment_score, source, meta_data) - VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?) - """, ( - str(item_id), - query_hash, - item.get('rank', 0), - item.get('title'), - item.get('url'), - item.get('content', ''), - item.get('publish_time'), - item.get('crawl_time') or current_time, - item.get('sentiment_score'), - item.get('source'), - json.dumps(item.get('meta_data', {})) - )) - except sqlite3.Error as e: - logger.error(f"Database error saving search detail {item.get('title')}: {e}") - except Exception as e: - logger.error(f"Unexpected error saving search detail {item.get('title')}: {e}") - - self.conn.commit() - - def find_similar_queries(self, query: str, limit: int = 5) -> List[Dict]: - """模糊搜索相似的已缓存查询""" - cursor = self.conn.cursor() - - # Simple fuzzy match: query in cached OR cached in query - q_wild = f"%{query}%" - cursor.execute(""" - SELECT query, query_hash, timestamp, results - FROM search_cache - WHERE query LIKE ? OR ? LIKE ('%' || query || '%') - ORDER BY timestamp DESC - LIMIT ? - """, (q_wild, query, limit)) - - return [dict(row) for row in cursor.fetchall()] - - def search_local_news(self, query: str, limit: int = 5) -> List[Dict]: - """从本地 daily_news 搜索相关新闻""" - cursor = self.conn.cursor() - q_wild = f"%{query}%" - # Search title and content - cursor.execute(""" - SELECT * FROM daily_news - WHERE title LIKE ? OR content LIKE ? - ORDER BY crawl_time DESC - LIMIT ? - """, (q_wild, q_wild, limit)) - return [dict(row) for row in cursor.fetchall()] - - # --- 股票数据操作 --- - - def save_stock_list(self, df: pd.DataFrame): - """保存股票列表到 stock_list 表""" - cursor = self.conn.cursor() - try: - # 清空旧表 - cursor.execute("DELETE FROM stock_list") - - # 批量插入 - data = df[['code', 'name']].to_dict('records') - cursor.executemany( - "INSERT INTO stock_list (code, name) VALUES (:code, :name)", - data - ) - self.conn.commit() - except sqlite3.Error as e: - logger.error(f"Database error saving stock list: {e}") - except Exception as e: - logger.error(f"Unexpected error saving stock list: {e}") - - def search_stock(self, query: str, limit: int = 5) -> List[Dict]: - """模糊搜索股票代码或名称""" - cursor = self.conn.cursor() - wild = f"%{query}%" - cursor.execute(""" - SELECT code, name FROM stock_list - WHERE code LIKE ? OR name LIKE ? - LIMIT ? - """, (wild, wild, limit)) - return [dict(row) for row in cursor.fetchall()] - - def get_stock_by_code(self, code: str) -> Optional[Dict[str, str]]: - """精确按代码获取股票信息。 - - Args: - code: 股票代码(A股6位 / 港股5位),必须为纯数字字符串。 - - Returns: - dict: {"code": str, "name": str} 或 None。 - """ - if not code: - return None - clean = "".join([c for c in str(code).strip() if c.isdigit()]) - if not clean: - return None - - cursor = self.conn.cursor() - cursor.execute("SELECT code, name FROM stock_list WHERE code = ? LIMIT 1", (clean,)) - row = cursor.fetchone() - return dict(row) if row else None - - def save_stock_prices(self, ticker: str, df: pd.DataFrame): - """保存股价历史数据""" - if df.empty: - return - - cursor = self.conn.cursor() - - # 确保 DataFrame 有必要的列 - required_cols = ['date', 'open', 'close', 'high', 'low', 'volume', 'change_pct'] - for col in required_cols: - if col not in df.columns: - logger.warning(f"Missing column {col} in stock data for {ticker}") - return - - try: - for _, row in df.iterrows(): - cursor.execute(""" - INSERT OR REPLACE INTO stock_prices - (ticker, date, open, close, high, low, volume, change_pct) - VALUES (?, ?, ?, ?, ?, ?, ?, ?) - """, ( - ticker, - row['date'], - row['open'], - row['close'], - row['high'], - row['low'], - row['volume'], - row['change_pct'] - )) - self.conn.commit() - except sqlite3.Error as e: - logger.error(f"Database error saving stock prices for {ticker}: {e}") - except Exception as e: - logger.error(f"Unexpected error saving stock prices for {ticker}: {e}") - - def get_stock_prices(self, ticker: str, start_date: str, end_date: str) -> pd.DataFrame: - """获取指定日期范围的股价数据""" - cursor = self.conn.cursor() - - cursor.execute(""" - SELECT * FROM stock_prices - WHERE ticker = ? AND date >= ? AND date <= ? - ORDER BY date - """, (ticker, start_date, end_date)) - - rows = cursor.fetchall() - if not rows: - return pd.DataFrame() - - columns = ['ticker', 'date', 'open', 'close', 'high', 'low', 'volume', 'change_pct'] - return pd.DataFrame([dict(row) for row in rows], columns=columns) - - def execute_query(self, query: str, params: tuple = ()) -> List[Any]: - """执行自定义 SQL 查询""" - try: - cursor = self.conn.cursor() - cursor.execute(query, params) - if query.strip().upper().startswith("SELECT"): - return cursor.fetchall() - else: - self.conn.commit() - return [] - except sqlite3.Error as e: - logger.error(f"SQL execution failed (Database error): {e}") - return [] - except Exception as e: - logger.error(f"SQL execution failed (Unexpected error): {e}") - return [] - - # --- 投资信号操作 (ISQ Framework) --- - - def save_signal(self, signal: Dict[str, Any]): - """保存投资信号""" - cursor = self.conn.cursor() - created_at = datetime.now().isoformat() - - cursor.execute(""" - INSERT OR REPLACE INTO signals - (signal_id, title, summary, transmission_chain, sentiment_score, - confidence, intensity, expected_horizon, price_in_status, - impact_tickers, industry_tags, sources, user_id, created_at) - VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?) - """, ( - signal.get('signal_id'), - signal.get('title'), - signal.get('summary'), - json.dumps(signal.get('transmission_chain', [])), - signal.get('sentiment_score', 0.0), - signal.get('confidence', 0.0), - signal.get('intensity', 1), - signal.get('expected_horizon', 'T+0'), - signal.get('price_in_status', '未知'), - json.dumps(signal.get('impact_tickers', [])), - json.dumps(signal.get('industry_tags', [])), - json.dumps(signal.get('sources', [])), - signal.get('user_id'), - created_at - )) - self.conn.commit() - - def get_recent_signals(self, limit: int = 20, user_id: Optional[str] = None) -> List[Dict]: - """获取最近的投资信号""" - cursor = self.conn.cursor() - if user_id: - cursor.execute("SELECT * FROM signals WHERE user_id = ? ORDER BY created_at DESC LIMIT ?", (user_id, limit)) - else: - cursor.execute("SELECT * FROM signals ORDER BY created_at DESC LIMIT ?", (limit,)) - rows = cursor.fetchall() - - signals = [] - for row in rows: - d = dict(row) - # 解析 JSON 字段 - for field in ['transmission_chain', 'impact_tickers', 'industry_tags', 'sources']: - if d.get(field): - try: - d[field] = json.loads(d[field]) - except: - pass - signals.append(d) - return signals - - def close(self): - if self.conn: - self.conn.close() - logger.info("Database connection closed.") - diff --git a/skills/alphaear-reporter/scripts/utils/hybrid_search.py b/skills/alphaear-reporter/scripts/utils/hybrid_search.py deleted file mode 100644 index c597fee..0000000 --- a/skills/alphaear-reporter/scripts/utils/hybrid_search.py +++ /dev/null @@ -1,216 +0,0 @@ -import numpy as np -import os -from typing import List, Dict, Any, Optional, Union -from rank_bm25 import BM25Okapi -from loguru import logger -from sentence_transformers import SentenceTransformer -from sklearn.metrics.pairwise import cosine_similarity - -class HybridSearcher: - """ - 统一混合检索引擎 (Hybrid RAG) - 实现 BM25 (文本) + 向量 (语义) 的融合搜索 (RRF) - """ - - def __init__(self, data: List[Dict[str, Any]], text_fields: List[str] = ["title", "content"], model_name: str = None): - """ - 初始化搜索器 - - Args: - data: 数据列表,每个元素为 Dict - text_fields: 用于建立索引的文本字段 - model_name: 向量模型名称,默认使用 paraphrase-multilingual-MiniLM-L12-v2 - """ - self.data = data - self.text_fields = text_fields - self._corpus = [] - self._bm25 = None - self._vector_model = None - self._embeddings = None - self._fitted = False - self._vector_fitted = False - - # 默认模型 - self.model_name = model_name or os.getenv("EMBEDDING_MODEL", "paraphrase-multilingual-MiniLM-L12-v2") - - if data: - self._prepare_corpus() - self._fit_bm25() - # 延迟加载向量模型,仅在需要时或初始化时显式调用 - # self._fit_vector() - - def _prepare_corpus(self): - """准备语料库用于分词""" - import jieba # 使用 jieba 进行中文分词 - - self._corpus = [] - self._full_texts = [] - for item in self.data: - text = " ".join([str(item.get(field, "")) for field in self.text_fields]) - self._full_texts.append(text) - # 中文分词优化 - tokens = list(jieba.cut(text)) - self._corpus.append(tokens) - - def _fit_bm25(self): - """训练 BM25 模型""" - if self._corpus: - self._bm25 = BM25Okapi(self._corpus) - self._fitted = True - logger.info(f"✅ BM25 index fitted with {len(self.data)} documents") - - def _fit_vector(self): - """训练向量模型并生成 Embeddings""" - if not self.data: - return - - try: - logger.info(f"📡 Loading embedding model: {self.model_name}...") - self._vector_model = SentenceTransformer(self.model_name) - logger.info(f"🧠 Encoding {len(self._full_texts)} documents...") - self._embeddings = self._vector_model.encode(self._full_texts, show_progress_bar=False) - self._vector_fitted = True - logger.info("✅ Vector index fitted successfully") - except Exception as e: - logger.error(f"❌ Failed to fit vector index: {e}") - self._vector_fitted = False - - def _compute_rrf(self, rank_lists: List[List[int]], k: int = 60) -> List[tuple]: - """ - 计算 Reciprocal Rank Fusion (RRF) - - Args: - rank_lists: 多个排序后的索引列表 - k: RRF 常数,默认 60 - """ - scores = {} - for rank_list in rank_lists: - for rank, idx in enumerate(rank_list): - if idx not in scores: - scores[idx] = 0 - scores[idx] += 1.0 / (k + rank + 1) - - # 按分数排序 - sorted_indices = sorted(scores.items(), key=lambda x: x[1], reverse=True) - return sorted_indices - - def search(self, query: str, top_n: int = 5, use_vector: bool = False) -> List[Dict[str, Any]]: - """ - 执行混合搜索 - - Args: - query: 搜索关键词 - top_n: 返回结果数量 - use_vector: 是否启用向量搜索 - """ - if not self._fitted or not query: - return [] - - import jieba - query_tokens = list(jieba.cut(query)) - - # 1. BM25 搜索结果 - bm25_scores = self._bm25.get_scores(query_tokens) - bm25_rank = np.argsort(bm25_scores)[::-1].tolist() - - rank_lists = [bm25_rank] - - # 2. 向量搜索逻辑 - if use_vector: - if not self._vector_fitted: - self._fit_vector() - - if self._vector_fitted: - query_embedding = self._vector_model.encode([query], show_progress_bar=False) - similarities = cosine_similarity(query_embedding, self._embeddings)[0] - vector_rank = np.argsort(similarities)[::-1].tolist() - rank_lists.append(vector_rank) - else: - logger.warning("Vector search requested but model not fitted, falling back to BM25") - - # 3. 融合排序 (RRF) - if len(rank_lists) > 1: - rrf_results = self._compute_rrf(rank_lists) - # RRF 返回 (idx, score) 列表 - final_rank = [idx for idx, score in rrf_results] - else: - final_rank = bm25_rank - - # 返回前 top_n 条结果 - results = [self.data[idx].copy() for idx in final_rank[:top_n]] - - # 为每个结果注入相关性评分 - for i, res in enumerate(results): - try: - original_idx = final_rank[i] - res["_search_score"] = bm25_scores[original_idx] - if use_vector and self._vector_fitted: - res["_vector_score"] = float(similarities[original_idx]) - except: - res["_search_score"] = 0 - - return results - -class InMemoryRAG(HybridSearcher): - """专门用于 ReportAgent 跨章节检索的内存态 RAG""" - - def search(self, query: str, top_n: int = 3, use_vector: bool = True) -> List[Dict[str, Any]]: - """默认开启向量搜索的内存检索""" - return super().search(query, top_n=top_n, use_vector=use_vector) - - def update_data(self, new_data: List[Dict[str, Any]]): - """动态更新数据并重新训练索引""" - self.data = new_data - self._prepare_corpus() - self._fit_bm25() - # 如果之前已经加载过向量模型,则更新向量索引 - if self._vector_model: - self._fit_vector() - logger.info(f"🔄 InMemoryRAG updated with {len(new_data)} items") - -class LocalNewsSearch(HybridSearcher): - """持久态 RAG:检索数据库中的历史新闻""" - - def __init__(self, db_manager): - """ - Args: - db_manager: DatabaseManager 实例 - """ - self.db = db_manager - # 初始时不加载数据,需调用 load_history - super().__init__([], ["title", "content"]) - - def load_history(self, days: int = 30, limit: int = 1000): - """从数据库加载最近 N 天的新闻构建索引""" - try: - # 假设 db_manager 有 execute_query - query = f"SELECT title, content, publish_time, source FROM daily_news ORDER BY publish_time DESC LIMIT ?" - results = self.db.execute_query(query, (limit,)) - - data = [] - for row in results: - # 转换 Row 为 Dict - if hasattr(row, 'keys'): - item = dict(row) - else: - item = { - "title": row[0], - "content": row[1], - "publish_time": row[2], - "source": row[3] - } - data.append(item) - - self.data = data - self._prepare_corpus() - self._fit_bm25() - # 默认不立即训练向量,等到第一次搜索时按需训练 - logger.info(f"📚 LocalNewsSearch loaded {len(data)} items from history") - except Exception as e: - logger.error(f"Failed to load history for search: {e}") - - def search(self, query: str, top_n: int = 5, use_vector: bool = True) -> List[Dict[str, Any]]: - """执行本地历史搜索,默认开启向量搜索""" - if not self.data: - self.load_history() - return super().search(query, top_n=top_n, use_vector=use_vector) diff --git a/skills/alphaear-reporter/scripts/utils/json_utils.py b/skills/alphaear-reporter/scripts/utils/json_utils.py deleted file mode 100644 index c29aab2..0000000 --- a/skills/alphaear-reporter/scripts/utils/json_utils.py +++ /dev/null @@ -1,180 +0,0 @@ -import ast -import json -import re -from typing import Optional, Any -from loguru import logger - -def _strip_comments(text: str) -> str: - """ - Safely remove C-style comments (// and /* */) from JSON-like text, - preserving strings (including URLs like http://). - """ - result = [] - i = 0 - n = len(text) - in_string = False - escape = False - - while i < n: - char = text[i] - - if in_string: - if char == '\\': - escape = not escape - elif char == '"' and not escape: - in_string = False - else: - escape = False - result.append(char) - i += 1 - continue - - # Not in string - if char == '"': - in_string = True - result.append(char) - i += 1 - continue - - # Check for // comment - if i + 1 < n and text[i:i+2] == '//': - i += 2 - while i < n and text[i] != '\n': - i += 1 - continue - - # Check for /* comment - if i + 1 < n and text[i:i+2] == '/*': - i += 2 - while i + 1 < n and text[i:i+2] != '*/': - i += 1 - i += 2 - continue - - result.append(char) - i += 1 - - return ''.join(result) - -def extract_json(text: str) -> Optional[Any]: - """ - 更加鲁棒的 JSON 提取工具。 - 处理: - 1. Markdown 代码块 (```json ... ```) - 2. 首尾多余字符 - 3. 同一个文本中多个 JSON 对象 (仅提取第一个) - 4. 简单的 JSON 修复 (末尾逗号等) - 5. C 风格注释 (// 和 /* */) - """ - if not text: - return None - - # 1. 清理明显的 Markdown 包装 - text = text.strip() - - # 先尝试精确匹配 ```json ... ``` 或 ```...``` - md_match = re.search(r'```(?:json)?\s*\n?(.*?)\n?```', text, re.DOTALL) - if md_match: - text = md_match.group(1).strip() - elif text.startswith("```"): - # 回退:如果开头有 ``` 但没完整匹配 - text = re.sub(r'^```[a-z]*\n?', '', text) - text = re.sub(r'\n?```\s*$', '', text) - - # 2. 寻找第一个 JSON 起始符 { 或 [ - start_brace = text.find('{') - start_bracket = text.find('[') - - if start_brace == -1 and start_bracket == -1: - return None - - start_idx = start_brace if (start_bracket == -1 or (start_brace != -1 and start_brace < start_bracket)) else start_bracket - - # 2.5 预处理:修复一些极其常见的 LLM 错误 - potential_json = text[start_idx:].strip() - - # remove comments safely - potential_json = _strip_comments(potential_json) - - # b. 修复缺失开头引号的键: nodes": [ -> "nodes": [ - # 匹配模式: (空白或换行) 单词 紧跟引号和冒号 - potential_json = re.sub(r'([\{\,]\s*)([a-zA-Z_]\w*)\"\s*:', r'\1"\2":', potential_json) - - # c. 修复缺失末尾引号的键: "nodes: [ -> "nodes": [ - potential_json = re.sub(r'([\{\,]\s*)\"([a-zA-Z_]\w*)\s*:', r'\1"\2":', potential_json) - - # d. 修复完全缺失引号的键: nodes: [ -> "nodes": [ - # 注意避免匹配到像 http:// 这种内容,所以限定在 { 或 , 之后 - potential_json = re.sub(r'([\{\,]\s*)([a-zA-Z_]\w*)\s*:', r'\1"\2":', potential_json) - - # 3. 使用 raw_decode 尝试解析 - decoder = json.JSONDecoder() - - # 首先尝试直接解析(不做任何预处理) - try: - obj = json.loads(potential_json) - return obj - except json.JSONDecodeError: - pass - - # 简单预处理:移除对象/列表末位多余逗号 - processed_json = re.sub(r',\s*([\]}])', r'\1', potential_json) - - try: - obj, end_pos = decoder.raw_decode(processed_json) - return obj - except json.JSONDecodeError: - pass - - # e. 修复未终止的字符串字面量问题:移除值中的实际换行符 - # LLM 可能在字符串值中生成包含真实 newline 的内容,导致 JSON 非法 - def fix_multiline_strings(s): - # 简单策略:将字符串值内的换行替换为空格 - lines = s.split('\n') - result = [] - in_string = False - for line in lines: - # 计算未转义的引号数 - quote_count = line.count('"') - line.count('\\"') - if in_string: - result[-1] += ' ' + line.strip() - else: - result.append(line) - - if quote_count % 2 == 1: - in_string = not in_string - return '\n'.join(result) - - fixed_json = fix_multiline_strings(processed_json) - - try: - obj, end_pos = decoder.raw_decode(fixed_json) - return obj - except json.JSONDecodeError: - try: - # 4. 尝试处理单引号问题 (JSON 规范要求双引号,但 LLM 常输出单引号) - # 这是一个简单的替换技巧,仅针对像 {'key': 'value'} 这样的结构 - # 注意:这可能会破坏包含单引号的字符串值,所以作为较后的回退 - fix_quotes = re.sub(r"'(.*?)':", r'"\1":', processed_json) # 修复键 - fix_quotes = re.sub(r":\s*'(.*?)'", r': "\1"', fix_quotes) # 修复简单值 - obj, end_pos = decoder.raw_decode(fix_quotes) - return obj - except (json.JSONDecodeError, TypeError): - try: - # 5. 使用 ast.literal_eval 作为终极回退 (处理 Python 字典格式) - # 提取第一个匹配的括号对内容 - # 寻找匹配的 { } - stack = [] - for i, char in enumerate(potential_json): - if char == '{': stack.append('{') - elif char == '}': - if stack: stack.pop() - if not stack: - content = potential_json[:i+1] - return ast.literal_eval(content) - except (ValueError, SyntaxError, MemoryError) as e: - logger.warning(f"All JSON extraction attempts failed: {e}") - except Exception as e: - logger.error(f"Unexpected error during JSON extraction: {e}") - - return None diff --git a/skills/alphaear-reporter/scripts/utils/llm/capability.py b/skills/alphaear-reporter/scripts/utils/llm/capability.py deleted file mode 100644 index d07ca4f..0000000 --- a/skills/alphaear-reporter/scripts/utils/llm/capability.py +++ /dev/null @@ -1,85 +0,0 @@ -import os -from typing import Optional, List, Dict, Any -from agno.agent import Agent -from agno.models.base import Model -from loguru import logger -from ..llm.factory import get_model - - -def test_tool_call_support(model: Model) -> bool: - """ - 测试模型是否支持原生的 Tool Call (Function Calling)。 - 通过尝试执行一个简单的加法工具来验证。 - """ - - def get_current_weather(location: str): - """获取指定地点的天气""" - return f"{location} 的天气是晴天,25度。" - - test_agent = Agent( - model=model, - tools=[get_current_weather], - instructions="请调用工具查询北京的天气,并直接返回工具的输出结果。", - ) - - try: - # 运行一个简单的任务,观察是否触发了 tool_call - response = test_agent.run("北京天气怎么样?") - - # 检查 response 中是否包含 tool_calls - # Agno 的 RunResponse 对象通常包含 messages,我们可以检查最后几条消息 - has_tool_call = False - for msg in response.messages: - if hasattr(msg, "tool_calls") and msg.tool_calls: - has_tool_call = True - break - - if has_tool_call: - logger.info(f"✅ Model {model.id} supports native tool calling.") - return True - else: - # 如果没有 tool_calls 但返回了正确答案,可能是模型通过纯文本模拟了工具调用(ReAct) - # 或者根本没用工具。对于原生支持的判断,我们坚持要求有 tool_calls 结构。 - logger.warning( - f"⚠️ Model {model.id} did NOT use native tool calling structure." - ) - return False - - except Exception as e: - logger.error(f"❌ Error testing tool call for {model.id}: {e}") - return False - - -class ModelCapabilityRegistry: - """ - 模型能力注册表,用于缓存和管理不同模型的能力测试结果。 - """ - - _cache = {} - - @classmethod - def get_capabilities( - cls, provider: str, model_id: str, **kwargs - ) -> Dict[str, bool]: - key = f"{provider}:{model_id}" - if key not in cls._cache: - logger.info(f"🔍 Testing capabilities for {key}...") - model = get_model(provider, model_id, **kwargs) - supports_tool_call = test_tool_call_support(model) - cls._cache[key] = {"supports_tool_call": supports_tool_call} - return cls._cache[key] - - -if __name__ == "__main__": - import os - from skills._env_loader import load_unified_env - - load_unified_env() - - # 测试当前配置的模型 - p = os.getenv("LLM_PROVIDER", "minimax") - m = os.getenv("LLM_MODEL", "Qwen") - - print(f"Testing {p}/{m}...") - res = ModelCapabilityRegistry.get_capabilities(p, m) - print(f"Result: {res}") diff --git a/skills/alphaear-reporter/scripts/utils/llm/factory.py b/skills/alphaear-reporter/scripts/utils/llm/factory.py deleted file mode 100644 index 09b6ea5..0000000 --- a/skills/alphaear-reporter/scripts/utils/llm/factory.py +++ /dev/null @@ -1,114 +0,0 @@ -import os -from agno.models.openai import OpenAIChat -from agno.models.ollama import Ollama -from agno.models.dashscope import DashScope -from agno.models.deepseek import DeepSeek -from agno.models.openrouter import OpenRouter - -def get_model(model_provider: str, model_id: str, **kwargs): - """ - Factory to get the appropriate LLM model. - - Args: - model_provider: "openai", "ollama", "deepseek" - model_id: The specific model ID (e.g., "gpt-4o", "llama3", "deepseek-chat") - **kwargs: Additional arguments for the model constructor - """ - if model_provider == "openai": - return OpenAIChat(id=model_id, **kwargs) - - elif model_provider == "ollama": - return Ollama(id=model_id, **kwargs) - - elif model_provider == "deepseek": - # DeepSeek is OpenAI compatible - api_key = os.getenv("DEEPSEEK_API_KEY") - if not api_key: - print("Warning: DEEPSEEK_API_KEY not set.") - - return DeepSeek( - id=model_id, - api_key=api_key, - **kwargs - ) - elif model_provider == "dashscope": - api_key = os.getenv("DASHSCOPE_API_KEY") - if not api_key: - print("Warning: DASHSCOPE_API_KEY not set.") - - return DashScope( - id=model_id, - base_url="https://dashscope.aliyuncs.com/compatible-mode/v1", - api_key=api_key, - **kwargs - ) - elif model_provider == 'openrouter': - api_key = os.getenv("OPENROUTER_API_KEY") - if not api_key: - print('Warning: OPENROUTER_API_KEY not set.') - - return OpenRouter( - id=model_id, - api_key=api_key, - **kwargs - ) - - elif model_provider == 'zai': - api_key = os.getenv("ZAI_KEY_API") - if not api_key: - print('Warning: ZAI_KEY_API not set.') - - # role_map to ensure compatibility. - default_role_map = { - "system": "system", - "user": "user", - "assistant": "assistant", - "tool": "tool", - "model": "assistant", - } - - # Allow callers to override role_map via kwargs, otherwise use default - role_map = kwargs.pop("role_map", default_role_map) - - return OpenAIChat( - id=model_id, - base_url="https://api.z.ai/api/paas/v4", - api_key=api_key, - timeout=60, - role_map=role_map, - extra_body={"enable_thinking": False}, # TODO: one more setting for thinking - **kwargs - ) - - elif model_provider == 'ust': - api_key = os.getenv("UST_KEY_API") - if not api_key: - print('Warning: UST_KEY_API not set.') - - # Some UST-compatible endpoints expect the standard OpenAI role names - # (e.g. "system", "user", "assistant") rather than Agno's default - # mapping which maps "system" -> "developer". Provide an explicit - # role_map to ensure compatibility. - default_role_map = { - "system": "system", - "user": "user", - "assistant": "assistant", - "tool": "tool", - "model": "assistant", - } - - # Allow callers to override role_map via kwargs, otherwise use default - role_map = kwargs.pop("role_map", default_role_map) - - return OpenAIChat( - id=model_id, - api_key=api_key, - base_url=os.getenv("UST_URL"), - role_map=role_map, - extra_body={"enable_thinking": False}, # TODO: one more setting for thinking - **kwargs - ) - - else: - raise ValueError(f"Unknown model provider: {model_provider}") - diff --git a/skills/alphaear-reporter/scripts/utils/llm/router.py b/skills/alphaear-reporter/scripts/utils/llm/router.py deleted file mode 100644 index 8c69958..0000000 --- a/skills/alphaear-reporter/scripts/utils/llm/router.py +++ /dev/null @@ -1,80 +0,0 @@ -import os -from typing import Optional, List, Dict, Any, Union -from agno.models.base import Model -from loguru import logger -from ..llm.factory import get_model -from ..llm.capability import ModelCapabilityRegistry -from skills._env_loader import load_unified_env - -load_unified_env() - - -class ModelRouter: - """ - 模型路由管理器 - - 功能: - 1. 管理“推理/写作模型” (Reasoning Model) 和“工具调用模型” (Tool Model)。 - 2. 根据任务需求自动选择合适的模型。 - """ - - def __init__(self): - # 默认从环境变量读取 - self.reasoning_provider = os.getenv( - "REASONING_MODEL_PROVIDER", os.getenv("LLM_PROVIDER", "openai") - ) - self.reasoning_id = os.getenv( - "REASONING_MODEL_ID", os.getenv("LLM_MODEL", "gpt-4o") - ) - self.reasoning_host = os.getenv("REASONING_MODEL_HOST", os.getenv("LLM_HOST")) - - self.tool_provider = os.getenv("TOOL_MODEL_PROVIDER", self.reasoning_provider) - self.tool_id = os.getenv("TOOL_MODEL_ID", self.reasoning_id) - self.tool_host = os.getenv("TOOL_MODEL_HOST", self.reasoning_host) - - self._reasoning_model = None - self._tool_model = None - - logger.info( - f"🤖 ModelRouter initialized: Reasoning={self.reasoning_id} ({self.reasoning_host or 'default'}), Tool={self.tool_id} ({self.tool_host or 'default'})" - ) - - def get_reasoning_model(self, **kwargs) -> Model: - if not self._reasoning_model: - # 优先使用路由配置的 host - if self.reasoning_host and "host" not in kwargs: - kwargs["host"] = self.reasoning_host - self._reasoning_model = get_model( - self.reasoning_provider, self.reasoning_id, **kwargs - ) - return self._reasoning_model - - def get_tool_model(self, **kwargs) -> Model: - if not self._tool_model: - # 优先使用路由配置的 host - if self.tool_host and "host" not in kwargs: - kwargs["host"] = self.tool_host - - # 检查 tool_model 是否真的支持 tool call - caps = ModelCapabilityRegistry.get_capabilities( - self.tool_provider, self.tool_id, **kwargs - ) - if not caps["supports_tool_call"]: - logger.warning( - f"⚠️ Configured tool model {self.tool_id} might not support native tool calls! Consider using ReAct mode or a different model." - ) - - self._tool_model = get_model(self.tool_provider, self.tool_id, **kwargs) - return self._tool_model - - def get_model_for_agent(self, has_tools: bool = False, **kwargs) -> Model: - """ - 根据 Agent 是否包含工具来返回合适的模型。 - """ - if has_tools: - return self.get_tool_model(**kwargs) - return self.get_reasoning_model(**kwargs) - - -# 全局单例 -router = ModelRouter() diff --git a/skills/alphaear-reporter/scripts/utils/logging_setup.py b/skills/alphaear-reporter/scripts/utils/logging_setup.py deleted file mode 100644 index 9a2ca62..0000000 --- a/skills/alphaear-reporter/scripts/utils/logging_setup.py +++ /dev/null @@ -1,45 +0,0 @@ -import os -import sys -from datetime import datetime -from typing import Optional - -from loguru import logger - - -def setup_file_logging( - run_id: str, - log_dir: str = "logs", - level: str = "INFO", - retention: str = "10 days", - rotation: str = "20 MB", -) -> str: - """Configure Loguru to log to stderr + a per-run file. - - Returns the log file path. - """ - os.makedirs(log_dir, exist_ok=True) - - # Remove default handler to avoid duplicate logs. - logger.remove() - - # Console - logger.add(sys.stderr, level=level, backtrace=False, diagnose=False) - - # File (safe for multi-thread via enqueue) - log_path = os.path.join(log_dir, f"signalflux_{run_id}.log") - logger.add( - log_path, - level=level, - rotation=rotation, - retention=retention, - enqueue=True, - backtrace=True, - diagnose=False, - encoding="utf-8", - ) - return log_path - - -def make_run_id(prefix: Optional[str] = None) -> str: - ts = datetime.now().strftime("%Y%m%d_%H%M%S") - return f"{prefix}_{ts}" if prefix else ts diff --git a/skills/alphaear-reporter/scripts/utils/news_tools.py b/skills/alphaear-reporter/scripts/utils/news_tools.py deleted file mode 100644 index e833e2e..0000000 --- a/skills/alphaear-reporter/scripts/utils/news_tools.py +++ /dev/null @@ -1,256 +0,0 @@ -import requests -from requests.exceptions import RequestException, Timeout -import json -import time -from datetime import datetime -from typing import List, Dict, Optional -from loguru import logger -from .database_manager import DatabaseManager -from .content_extractor import ContentExtractor - -class NewsNowTools: - """热点新闻获取工具 - 接入 NewsNow API 与 Jina 内容提取""" - - BASE_URL = "https://newsnow.busiyi.world" - SOURCES = { - # 金融类 - "cls": "财联社", - "wallstreetcn": "华尔街见闻", - "xueqiu": "雪球热榜", - # 综合/社交 - "weibo": "微博热搜", - "zhihu": "知乎热榜", - "baidu": "百度热搜", - "toutiao": "今日头条", - "douyin": "抖音热榜", - "thepaper": "澎湃新闻", - # 科技类 - "36kr": "36氪", - "ithome": "IT之家", - "v2ex": "V2EX", - "juejin": "掘金", - "hackernews": "Hacker News", - } - - - def __init__(self, db: DatabaseManager): - self.db = db - self.user_agent = ( - "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) " - "AppleWebKit/537.36 (KHTML, like Gecko) Chrome/131.0.0.0 Safari/537.36" - ) - self.extractor = ContentExtractor() - # Simple in-memory cache: source_id -> {"time": timestamp, "data": []} - self._cache = {} - - def fetch_hot_news(self, source_id: str, count: int = 15, fetch_content: bool = False) -> List[Dict]: - """ - 从指定新闻源获取热点新闻列表(支持5分钟缓存)。 - """ - # 1. Check cache validity (5 minutes) - cache_key = f"{source_id}_{count}" - cached = self._cache.get(cache_key) - now = time.time() - - if cached and (now - cached["time"] < 300): - logger.info(f"⚡ Using cached news for {source_id} (Age: {int(now - cached['time'])}s)") - return cached["data"] - - try: - url = f"{self.BASE_URL}/api/s?id={source_id}" - response = requests.get(url, headers={"User-Agent": self.user_agent}, timeout=30) - if response.status_code == 200: - data = response.json() - items = data.get("items", [])[:count] - processed_items = [] - for i, item in enumerate(items, 1): - item_url = item.get("url", "") - content = "" - if fetch_content and item_url: - content = self.extractor.extract_with_jina(item_url) or "" - - processed_items.append({ - "id": item.get("id") or f"{source_id}_{int(time.time())}_{i}", - "source": source_id, - "rank": i, - "title": item.get("title", ""), - "url": item_url, - "content": content, - "publish_time": item.get("publish_time"), - "meta_data": item.get("extra", {}) - }) - - # Update Cache - self._cache[cache_key] = {"time": now, "data": processed_items} - logger.info(f"✅ Fetched and cached news for {source_id}") - - self.db.save_daily_news(processed_items) - return processed_items - else: - logger.error(f"NewsNow API Error: {response.status_code}") - # Fallback to stale cache if available - if cached: - logger.warning(f"⚠️ API failed, using stale cache for {source_id}") - return cached["data"] - return [] - except Timeout: - logger.error(f"Timeout fetching hot news from {source_id}") - if cached: - logger.warning(f"⚠️ Timeout, using stale cache for {source_id}") - return cached["data"] - return [] - except RequestException as e: - logger.error(f"Network error fetching hot news from {source_id}: {e}") - if cached: - logger.warning(f"⚠️ Network check failed, using stale cache for {source_id}") - return cached["data"] - return [] - except json.JSONDecodeError: - logger.error(f"Failed to parse JSON response from NewsNow for {source_id}") - return [] - except Exception as e: - logger.error(f"Unexpected error fetching hot news from {source_id}: {e}") - return [] - - def fetch_news_content(self, url: str) -> Optional[str]: - """ - 使用 Jina Reader 抓取指定 URL 的网页正文内容。 - - Args: - url: 需要抓取内容的完整网页 URL,必须以 http:// 或 https:// 开头。 - - Returns: - 提取的网页正文内容 (Markdown 格式),如果失败则返回 None。 - """ - return self.extractor.extract_with_jina(url) - - def get_unified_trends(self, sources: Optional[List[str]] = None) -> str: - """ - 获取多平台综合热点报告,自动聚合多个新闻源的热门内容。 - - Args: - sources: 要扫描的新闻源列表。可选值按类别: - **金融类**: "cls", "wallstreetcn", "xueqiu" - **综合类**: "weibo", "zhihu", "baidu", "toutiao", "douyin", "thepaper" - **科技类**: "36kr", "ithome", "v2ex", "juejin", "hackernews" - - Returns: - 格式化的 Markdown 热点汇总报告,包含各平台 Top 10 热点标题和链接。 - """ - sources = sources or ["weibo", "zhihu", "wallstreetcn"] - all_news = [] - for src in sources: - all_news.extend(self.fetch_hot_news(src)) - time.sleep(0.2) - - if not all_news: - return "❌ 未能获取到热点数据" - - report = f"# 实时全网热点汇总 ({datetime.now().strftime('%Y-%m-%d %H:%M')})\n\n" - for src in sources: - - src_name = self.SOURCES.get(src, src) - report += f"### 🔥 {src_name}\n" - src_news = [n for n in all_news if n['source'] == src] - for n in src_news[:10]: - report += f"- {n['title']} ([链接]({n['url']}))\n" - report += "\n" - - return report - - -class PolymarketTools: - """Polymarket 预测市场数据工具 - 获取热门预测市场反映公众情绪和预期""" - - BASE_URL = "https://gamma-api.polymarket.com" - - def __init__(self, db: DatabaseManager): - self.db = db - self.user_agent = "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36" - - def get_active_markets(self, limit: int = 20) -> List[Dict]: - """ - 获取活跃的预测市场,用于分析公众情绪和预期。 - - 预测市场数据可以反映: - - 公众对重大事件的预期概率 - - 市场情绪和风险偏好 - - 热门话题的关注度 - - Args: - limit: 获取的市场数量,默认 20 个。 - - Returns: - 包含预测市场信息的列表,每个市场包含: - - question: 预测问题 - - outcomes: 可能的结果 - - outcomePrices: 各结果的概率价格 - - volume: 交易量 - """ - try: - response = requests.get( - f"{self.BASE_URL}/markets", - params={"active": "true", "closed": "false", "limit": limit}, - headers={"User-Agent": self.user_agent, "Accept": "application/json"}, - timeout=30 - ) - - if response.status_code == 200: - markets = response.json() - result = [] - for m in markets: - result.append({ - "id": m.get("id"), - "question": m.get("question"), - "slug": m.get("slug"), - "outcomes": m.get("outcomes"), - "outcomePrices": m.get("outcomePrices"), - "volume": m.get("volume"), - "liquidity": m.get("liquidity"), - }) - logger.info(f"✅ 获取 {len(result)} 个预测市场") - return result - else: - logger.warning(f"⚠️ Polymarket API 返回 {response.status_code}") - return [] - except Timeout: - logger.error("Timeout fetching Polymarket markets") - return [] - except RequestException as e: - logger.error(f"Network error fetching Polymarket markets: {e}") - return [] - except json.JSONDecodeError: - logger.error("Failed to parse JSON response from Polymarket") - return [] - except Exception as e: - logger.error(f"Unexpected error fetching Polymarket markets: {e}") - return [] - - def get_market_summary(self, limit: int = 10) -> str: - """ - 获取预测市场摘要报告,用于了解当前热门话题和公众预期。 - - Args: - limit: 获取的市场数量 - - Returns: - 格式化的预测市场报告 - """ - markets = self.get_active_markets(limit) - if not markets: - return "❌ 无法获取 Polymarket 数据" - - report = f"# 🔮 Polymarket 热门预测 ({datetime.now().strftime('%Y-%m-%d %H:%M')})\n\n" - for i, m in enumerate(markets, 1): - question = m.get("question", "Unknown") - prices = m.get("outcomePrices", []) - volume = m.get("volume", 0) - - report += f"**{i}. {question}**\n" - if prices: - report += f" 概率: {prices}\n" - if volume: - report += f" 交易量: ${float(volume):,.0f}\n" - report += "\n" - - return report diff --git a/skills/alphaear-reporter/scripts/utils/predictor/evaluation.py b/skills/alphaear-reporter/scripts/utils/predictor/evaluation.py deleted file mode 100644 index 26c5df7..0000000 --- a/skills/alphaear-reporter/scripts/utils/predictor/evaluation.py +++ /dev/null @@ -1,137 +0,0 @@ -import os -import sys -import torch -import pandas as pd -import numpy as np -import glob -from loguru import logger -from datetime import datetime, timedelta - -# Setup paths -KRONOS_DIR = os.path.dirname(os.path.abspath(__file__)) -SRC_DIR = os.path.dirname(os.path.dirname(KRONOS_DIR)) -if SRC_DIR not in sys.path: - sys.path.insert(0, SRC_DIR) - -from ..kronos.auto_synthesis_training import AutoSynthesisTrainer -from ..kronos.model import KronosPredictor -from ..visualizer import VisualizerTools -from ..schema.models import ForecastResult, KLinePoint - -class NewsModelEvaluator: - def __init__(self, model_path=None): - self.trainer = AutoSynthesisTrainer() - self.device = self.trainer.device - - if model_path is None: - # Try to find the latest model in exports/models - model_files = glob.glob(os.path.join(SRC_DIR, "exports/models/*.pt")) - if not model_files: - logger.warning("⚠️ No trained models found in exports/models/. Using base model (zero-init proj).") - else: - model_path = max(model_files, key=os.path.getctime) - - if model_path: - self.load_weights(model_path) - - def load_weights(self, path): - logger.info(f"🔄 Loading model weights from {path}...") - checkpoint = torch.load(path, map_location=self.device) - self.trainer.model.news_proj.load_state_dict(checkpoint['news_proj_state_dict']) - logger.success("✅ News projection layer loaded.") - - def evaluate_range(self, start_idx=100, end_idx=200, pred_len=5): - # 1. Fetch Tickers - res = self.trainer.db.execute_query("SELECT code FROM stock_list") - all_tickers = [row['code'] for row in res] - test_tickers = all_tickers[start_idx:end_idx] - - if not test_tickers: - logger.error(f"No tickers found in range {start_idx}-{end_idx}") - return - - logger.info(f"🚀 Evaluating News Model on stocks {start_idx} to {end_idx}...") - - # 2. Discover Shocks - shocks = self.trainer.discover_shocks(test_tickers, pred_len=pred_len) - - # 3. Associate News & Predict - self.trainer.model.eval() - predictor = KronosPredictor(self.trainer.model, self.trainer.tokenizer, device=self.device) - - save_dir = os.path.join(SRC_DIR, "exports/evaluation_results") - os.makedirs(save_dir, exist_ok=True) - - count = 0 - for shock in shocks: - summary = self.trainer.find_reason_and_verify(shock) - if not summary: - continue - - logger.info(f"📈 Testing shock: {shock['ticker']} on {shock['date']}") - - # Embedding news - news_emb = self.trainer.embedder.encode(summary) - - # Prediction - h = shock['history'] - t = shock['target'] - actuals = t['close'].values[:pred_len] - - x_ts = pd.to_datetime(h['date']) - future_dates = pd.date_range(start=x_ts.iloc[-1] + timedelta(days=1), periods=pred_len, freq='B') - y_ts = pd.Series(future_dates) - - # A. Base Prediction (No news) - p_base = predictor.predict(h, x_ts, y_ts, pred_len=pred_len, news_emb=None, verbose=False) - - # B. News-Aware Prediction - p_news = predictor.predict(h, x_ts, y_ts, pred_len=pred_len, news_emb=news_emb, verbose=False) - - # Calculate Improvement - b_preds = p_base['close'].values[:len(actuals)] - n_preds = p_news['close'].values[:len(actuals)] - b_mae = np.mean(np.abs(b_preds - actuals)) - n_mae = np.mean(np.abs(n_preds - actuals)) - improvement = (b_mae - n_mae) / (b_mae + 1e-6) * 100 - - # C. Visualize - try: - def to_kp_list(preds_df): - points = [] - for idx, row in preds_df.iterrows(): - points.append(KLinePoint( - date=str(idx)[:10], open=row['open'], high=row['high'], - low=row['low'], close=row['close'], volume=row.get('volume', 0) - )) - return points - - forecast_obj = ForecastResult( - ticker=shock['ticker'], - base_forecast=to_kp_list(p_base), - adjusted_forecast=to_kp_list(p_news), - rationale=summary - ) - - chart = VisualizerTools.generate_stock_chart( - df=h, ticker=shock['ticker'], - title=f"Test Eval: {shock['ticker']} ({shock['date']}) Imp: {improvement:.1f}%", - forecast=forecast_obj, - ground_truth=t[['date', 'open', 'high', 'low', 'close', 'volume']] - ) - - safe_date = shock['date'].replace("-", "") - filename = f"test_{shock['ticker']}_{safe_date}.html" - VisualizerTools.render_chart_to_file(chart, os.path.join(save_dir, filename)) - - logger.success(f"📊 Result for {shock['ticker']} saved. Base MAE: {b_mae:.4f}, News MAE: {n_mae:.4f}") - count += 1 - except Exception as e: - logger.error(f"Visualization failed: {e}") - - logger.info(f"🏁 Finished evaluation. {count} cases visualized in {save_dir}") - -if __name__ == "__main__": - # If you have a specific model, pass the path here. Otherwise it picks the latest. - evaluator = NewsModelEvaluator() - evaluator.evaluate_range(start_idx=100, end_idx=200, pred_len=1) diff --git a/skills/alphaear-reporter/scripts/utils/predictor/kline_generate.py b/skills/alphaear-reporter/scripts/utils/predictor/kline_generate.py deleted file mode 100644 index 3224c21..0000000 --- a/skills/alphaear-reporter/scripts/utils/predictor/kline_generate.py +++ /dev/null @@ -1,196 +0,0 @@ -# Ref: https://github.com/shiyu-coder/Kronos - -from model import Kronos, KronosTokenizer, KronosPredictor -import pandas as pd -import sqlite3 -import torch -import matplotlib.pyplot as plt -import matplotlib.gridspec as gridspec -from pandas.tseries.offsets import BusinessDay -import numpy as np - -def get_device(): - device = "cuda" if torch.cuda.is_available() else "mps" if torch.backends.mps.is_available() else "cpu" - print(f"Using device: {device}") - return device - -def load_predictor(): - tokenizer = KronosTokenizer.from_pretrained("NeoQuasar/Kronos-Tokenizer-base") - model = Kronos.from_pretrained("NeoQuasar/Kronos-base") - device = get_device() - tokenizer = tokenizer.to(device) - model = model.to(device) - return KronosPredictor(model, tokenizer, device=device, max_context=512) - -def load_data(ticker="002111", db_path="AlphaEar/data/signal_flux.db"): - with sqlite3.connect(db_path) as conn: - df = pd.read_sql_query(f"SELECT * FROM stock_prices WHERE ticker = '{ticker}'", conn) - df['date'] = pd.to_datetime(df['date']) - df = df.sort_values('date').reset_index(drop=True) - return df - -def plot_kline_matplotlib(ax, ax_vol, dates, df, label_suffix="", color_up='#ef4444', color_down='#22c55e', alpha=1.0, is_prediction=False): - """ - 绘制 K 线图和成交量 - """ - # X axis mapping to integers for consistent spacing - x = np.arange(len(dates)) - - # K-line data - opens = df['open'].values - closes = df['close'].values - highs = df['high'].values - lows = df['low'].values - volumes = df['volume'].values - - # Width of the candlestick - width = 0.6 - - for i in range(len(x)): - color = color_up if closes[i] >= opens[i] else color_down - linestyle = '--' if is_prediction else '-' - - # Wick - ax.vlines(x[i], lows[i], highs[i], color=color, linewidth=1, alpha=alpha, linestyle=linestyle) - - # Body - rect_bottom = min(opens[i], closes[i]) - rect_height = abs(opens[i] - closes[i]) - if rect_height == 0: rect_height = 0.001 # Visual hair - - ax.add_patch(plt.Rectangle((x[i] - width/2, rect_bottom), width, rect_height, - edgecolor=color, facecolor=color if not is_prediction else 'none', - alpha=alpha, linewidth=1, linestyle=linestyle)) - - # Volume - ax_vol.bar(x[i], volumes[i], color=color, alpha=alpha * 0.5, width=width) - -def render_comparison_chart(history_df, actual_df, pred_df, title): - """ - 渲染组合图:历史 K 线 + 真值 K 线 + 预测 K 线 - """ - # Combine all dates for X axis - all_dates = pd.concat([history_df['date'], actual_df['date'] if actual_df is not None else pred_df.index.to_series()]).unique() - all_dates = sorted(all_dates) - date_to_idx = {date: i for i, date in enumerate(all_dates)} - - fig = plt.figure(figsize=(14, 8), facecolor='white') - gs = gridspec.GridSpec(2, 1, height_ratios=[3, 1], hspace=0.1) - ax_main = fig.add_subplot(gs[0]) - ax_vol = fig.add_subplot(gs[1], sharex=ax_main) - - # 1. Plot History - hist_indices = [date_to_idx[d] for d in history_df['date']] - # We use a custom x for plotting to ensure continuity - plot_kline_matplotlib(ax_main, ax_vol, history_df['date'], history_df, alpha=0.8) - - offset = len(history_df) - - # 2. Plot Actual if exists - if actual_df is not None: - # Shift indices - actual_x = np.arange(len(actual_df)) + offset - # Plotting manually to handle offset - for i in range(len(actual_df)): - idx = actual_x[i] - row = actual_df.iloc[i] - color = '#ef4444' if row['close'] >= row['open'] else '#22c55e' - ax_main.vlines(idx, row['low'], row['high'], color=color, linewidth=1, alpha=0.9) - ax_main.add_patch(plt.Rectangle((idx - 0.3, min(row['open'], row['close'])), 0.6, abs(row['open']-row['close']), - edgecolor=color, facecolor=color, alpha=0.9)) - ax_vol.bar(idx, row['volume'], color=color, alpha=0.4) - - # 3. Plot Prediction - pred_x = np.arange(len(pred_df)) + offset - for i in range(len(pred_df)): - idx = pred_x[i] - row = pred_df.iloc[i] - color = '#ff8c00' # Orange for prediction to distinguish - ax_main.vlines(idx, row['low'], row['high'], color=color, linewidth=1.5, linestyle='--') - ax_main.add_patch(plt.Rectangle((idx - 0.3, min(row['open'], row['close'])), 0.6, abs(row['open']-row['close']), - edgecolor=color, facecolor='none', linewidth=1.5, linestyle='--')) - # Plot secondary prediction line for close - if i == 0: - # Connect to history - ax_main.plot([offset-1, idx], [history_df['close'].iloc[-1], row['close']], color=color, linestyle='--', alpha=0.6) - elif i > 0: - ax_main.plot([idx-1, idx], [pred_df['close'].iloc[i-1], row['close']], color=color, linestyle='--', alpha=0.6) - - # Styling - ax_main.set_title(title, fontsize=14, fontweight='bold') - ax_main.grid(True, linestyle=':', alpha=0.6) - ax_vol.grid(True, linestyle=':', alpha=0.6) - ax_vol.set_ylabel('Volume') - ax_main.set_ylabel('Price') - - # Set X ticks - step = max(1, len(all_dates) // 10) - ax_vol.set_xticks(np.arange(0, len(all_dates), step)) - ax_vol.set_xticklabels([all_dates[i].strftime('%Y-%m-%d') for i in range(0, len(all_dates), step)], rotation=45) - - plt.tight_layout() - plt.show() - plt.close() - -def run_backtest(df, predictor, lookback, pred_len, start_index=0): - total_len = len(df) - history_start = start_index - history_end = start_index + lookback - pred_start = history_end - - available_pred_len = total_len - pred_start - if available_pred_len <= 0: return - actual_pred_len = min(pred_len, available_pred_len) - pred_end = pred_start + actual_pred_len - - x_df = df.iloc[history_start : history_end].copy() - y_true_df = df.iloc[pred_start : pred_end].copy() - y_timestamp = y_true_df['date'] - - print(f"Backtesting: {x_df['date'].iloc[0].date()} to {y_timestamp.iloc[-1].date()}") - - pred_df = predictor.predict( - df=x_df[['open', 'high', 'low', 'close', 'volume']], - x_timestamp=x_df['date'], - y_timestamp=y_timestamp, - pred_len=actual_pred_len, - T=1.0, top_p=0.9, sample_count=1 - ) - - render_comparison_chart(x_df, y_true_df, pred_df, f"Backtest: {TICKER} K-Line Comparison") - -def run_forecast(df, predictor, lookback, pred_len): - if len(df) < lookback: return - x_df = df.iloc[-lookback:].copy() - last_date = x_df['date'].iloc[-1] - future_dates = pd.date_range(start=last_date + BusinessDay(1), periods=pred_len, freq='B') - future_dates = pd.Series(future_dates) - - print(f"Forecasting: Starting from {future_dates.iloc[0].date()}") - - pred_df = predictor.predict( - df=x_df[['open', 'high', 'low', 'close', 'volume']], - x_timestamp=x_df['date'], - y_timestamp=future_dates, - pred_len=pred_len, - T=1.0, top_p=0.9, sample_count=1 - ) - - render_comparison_chart(x_df, None, pred_df, f"Forecast: {TICKER} Future K-Line") - -if __name__ == "__main__": - LOOKBACK = 20 - PRED_LEN = 10 - TICKER = '002111' - - pred_model = load_predictor() - stock_data = load_data(TICKER) - - total_rows = len(stock_data) - backtest_start = max(0, total_rows - LOOKBACK - PRED_LEN - 10) # Leave some space to see trend - - print("\n--- Running Backtest ---") - run_backtest(stock_data, pred_model, LOOKBACK, PRED_LEN, start_index=backtest_start) - - print("\n--- Running Forecast ---") - run_forecast(stock_data, pred_model, LOOKBACK, PRED_LEN) \ No newline at end of file diff --git a/skills/alphaear-reporter/scripts/utils/predictor/model/__init__.py b/skills/alphaear-reporter/scripts/utils/predictor/model/__init__.py deleted file mode 100644 index d10e200..0000000 --- a/skills/alphaear-reporter/scripts/utils/predictor/model/__init__.py +++ /dev/null @@ -1,16 +0,0 @@ -from .kronos import KronosTokenizer, Kronos, KronosPredictor - -model_dict = { - 'kronos_tokenizer': KronosTokenizer, - 'kronos': Kronos, - 'kronos_predictor': KronosPredictor -} - - -def get_model_class(model_name): - if model_name in model_dict: - return model_dict[model_name] - else: - print(f"Model {model_name} not found in model_dict") - raise NotImplementedError - diff --git a/skills/alphaear-reporter/scripts/utils/predictor/model/kronos.py b/skills/alphaear-reporter/scripts/utils/predictor/model/kronos.py deleted file mode 100644 index cf8bece..0000000 --- a/skills/alphaear-reporter/scripts/utils/predictor/model/kronos.py +++ /dev/null @@ -1,676 +0,0 @@ -import numpy as np -import pandas as pd -import torch -from huggingface_hub import PyTorchModelHubMixin -import sys - -from tqdm import trange - -sys.path.append("../") -from model.module import * - - -class KronosTokenizer(nn.Module, PyTorchModelHubMixin): - """ - KronosTokenizer module for tokenizing input data using a hybrid quantization approach. - - This tokenizer utilizes a combination of encoder and decoder Transformer blocks - along with the Binary Spherical Quantization (BSQuantizer) to compress and decompress input data. - - Args: - d_in (int): Input dimension. - d_model (int): Model dimension. - n_heads (int): Number of attention heads. - ff_dim (int): Feed-forward dimension. - n_enc_layers (int): Number of encoder layers. - n_dec_layers (int): Number of decoder layers. - ffn_dropout_p (float): Dropout probability for feed-forward networks. - attn_dropout_p (float): Dropout probability for attention mechanisms. - resid_dropout_p (float): Dropout probability for residual connections. - s1_bits (int): Number of bits for the pre token in BSQuantizer. - s2_bits (int): Number of bits for the post token in BSQuantizer. - beta (float): Beta parameter for BSQuantizer. - gamma0 (float): Gamma0 parameter for BSQuantizer. - gamma (float): Gamma parameter for BSQuantizer. - zeta (float): Zeta parameter for BSQuantizer. - group_size (int): Group size parameter for BSQuantizer. - - """ - - def __init__(self, d_in, d_model, n_heads, ff_dim, n_enc_layers, n_dec_layers, ffn_dropout_p, attn_dropout_p, resid_dropout_p, s1_bits, s2_bits, beta, gamma0, gamma, zeta, group_size): - - super().__init__() - self.d_in = d_in - self.d_model = d_model - self.n_heads = n_heads - self.ff_dim = ff_dim - self.enc_layers = n_enc_layers - self.dec_layers = n_dec_layers - self.ffn_dropout_p = ffn_dropout_p - self.attn_dropout_p = attn_dropout_p - self.resid_dropout_p = resid_dropout_p - - self.s1_bits = s1_bits - self.s2_bits = s2_bits - self.codebook_dim = s1_bits + s2_bits # Total dimension of the codebook after quantization - self.embed = nn.Linear(self.d_in, self.d_model) - self.head = nn.Linear(self.d_model, self.d_in) - - # Encoder Transformer Blocks - self.encoder = nn.ModuleList([ - TransformerBlock(self.d_model, self.n_heads, self.ff_dim, self.ffn_dropout_p, self.attn_dropout_p, self.resid_dropout_p) - for _ in range(self.enc_layers - 1) - ]) - # Decoder Transformer Blocks - self.decoder = nn.ModuleList([ - TransformerBlock(self.d_model, self.n_heads, self.ff_dim, self.ffn_dropout_p, self.attn_dropout_p, self.resid_dropout_p) - for _ in range(self.dec_layers - 1) - ]) - self.quant_embed = nn.Linear(in_features=self.d_model, out_features=self.codebook_dim) # Linear layer before quantization - self.post_quant_embed_pre = nn.Linear(in_features=self.s1_bits, out_features=self.d_model) # Linear layer after quantization (pre part - s1 bits) - self.post_quant_embed = nn.Linear(in_features=self.codebook_dim, out_features=self.d_model) # Linear layer after quantization (full codebook) - self.tokenizer = BSQuantizer(self.s1_bits, self.s2_bits, beta, gamma0, gamma, zeta, group_size) # BSQuantizer module - - def forward(self, x): - """ - Forward pass of the KronosTokenizer. - - Args: - x (torch.Tensor): Input tensor of shape (batch_size, seq_len, d_in). - - Returns: - tuple: A tuple containing: - - tuple: (z_pre, z) - Reconstructed outputs from decoder with s1_bits and full codebook respectively, - both of shape (batch_size, seq_len, d_in). - - torch.Tensor: bsq_loss - Loss from the BSQuantizer. - - torch.Tensor: quantized - Quantized representation from BSQuantizer. - - torch.Tensor: z_indices - Indices from the BSQuantizer. - """ - z = self.embed(x) - - for layer in self.encoder: - z = layer(z) - - z = self.quant_embed(z) # (B, T, codebook) - - bsq_loss, quantized, z_indices = self.tokenizer(z) - - quantized_pre = quantized[:, :, :self.s1_bits] # Extract the first part of quantized representation (s1_bits) - z_pre = self.post_quant_embed_pre(quantized_pre) - - z = self.post_quant_embed(quantized) - - # Decoder layers (for pre part - s1 bits) - for layer in self.decoder: - z_pre = layer(z_pre) - z_pre = self.head(z_pre) - - # Decoder layers (for full codebook) - for layer in self.decoder: - z = layer(z) - z = self.head(z) - - return (z_pre, z), bsq_loss, quantized, z_indices - - def indices_to_bits(self, x, half=False): - """ - Converts indices to bit representations and scales them. - - Args: - x (torch.Tensor): Indices tensor. - half (bool, optional): Whether to process only half of the codebook dimension. Defaults to False. - - Returns: - torch.Tensor: Bit representation tensor. - """ - if half: - x1 = x[0] # Assuming x is a tuple of indices if half is True - x2 = x[1] - mask = 2 ** torch.arange(self.codebook_dim//2, device=x1.device, dtype=torch.long) # Create a mask for bit extraction - x1 = (x1.unsqueeze(-1) & mask) != 0 # Extract bits for the first half - x2 = (x2.unsqueeze(-1) & mask) != 0 # Extract bits for the second half - x = torch.cat([x1, x2], dim=-1) # Concatenate the bit representations - else: - mask = 2 ** torch.arange(self.codebook_dim, device=x.device, dtype=torch.long) # Create a mask for bit extraction - x = (x.unsqueeze(-1) & mask) != 0 # Extract bits - - x = x.float() * 2 - 1 # Convert boolean to bipolar (-1, 1) - q_scale = 1. / (self.codebook_dim ** 0.5) # Scaling factor - x = x * q_scale - return x - - def encode(self, x, half=False): - """ - Encodes the input data into quantized indices. - - Args: - x (torch.Tensor): Input tensor of shape (batch_size, seq_len, d_in). - half (bool, optional): Whether to use half quantization in BSQuantizer. Defaults to False. - - Returns: - torch.Tensor: Quantized indices from BSQuantizer. - """ - z = self.embed(x) - for layer in self.encoder: - z = layer(z) - z = self.quant_embed(z) - - bsq_loss, quantized, z_indices = self.tokenizer(z, half=half, collect_metrics=False) - return z_indices - - def decode(self, x, half=False): - """ - Decodes quantized indices back to the input data space. - - Args: - x (torch.Tensor): Quantized indices tensor. - half (bool, optional): Whether the indices were generated with half quantization. Defaults to False. - - Returns: - torch.Tensor: Reconstructed output tensor of shape (batch_size, seq_len, d_in). - """ - quantized = self.indices_to_bits(x, half) - z = self.post_quant_embed(quantized) - for layer in self.decoder: - z = layer(z) - z = self.head(z) - return z - - -class Kronos(nn.Module, PyTorchModelHubMixin): - """ - Kronos Model. - - Args: - s1_bits (int): Number of bits for pre tokens. - s2_bits (int): Number of bits for post tokens. - n_layers (int): Number of Transformer blocks. - d_model (int): Dimension of the model's embeddings and hidden states. - n_heads (int): Number of attention heads in the MultiheadAttention layers. - ff_dim (int): Dimension of the feedforward network in the Transformer blocks. - ffn_dropout_p (float): Dropout probability for the feedforward network. - attn_dropout_p (float): Dropout probability for the attention layers. - resid_dropout_p (float): Dropout probability for residual connections. - token_dropout_p (float): Dropout probability for token embeddings. - learn_te (bool): Whether to use learnable temporal embeddings. - """ - - def __init__(self, s1_bits, s2_bits, n_layers, d_model, n_heads, ff_dim, ffn_dropout_p, attn_dropout_p, resid_dropout_p, token_dropout_p, learn_te, news_dim=None): - super().__init__() - self.s1_bits = s1_bits - self.s2_bits = s2_bits - self.n_layers = n_layers - self.d_model = d_model - self.n_heads = n_heads - self.learn_te = learn_te - self.ff_dim = ff_dim - self.ffn_dropout_p = ffn_dropout_p - self.attn_dropout_p = attn_dropout_p - self.resid_dropout_p = resid_dropout_p - self.token_dropout_p = token_dropout_p - self.news_dim = news_dim - - self.s1_vocab_size = 2 ** self.s1_bits - self.token_drop = nn.Dropout(self.token_dropout_p) - self.embedding = HierarchicalEmbedding(self.s1_bits, self.s2_bits, self.d_model) - self.time_emb = TemporalEmbedding(self.d_model, self.learn_te) - self.transformer = nn.ModuleList([ - TransformerBlock(self.d_model, self.n_heads, self.ff_dim, self.ffn_dropout_p, self.attn_dropout_p, self.resid_dropout_p) - for _ in range(self.n_layers) - ]) - self.norm = RMSNorm(self.d_model) - self.dep_layer = DependencyAwareLayer(self.d_model) - self.head = DualHead(self.s1_bits, self.s2_bits, self.d_model) - - if self.news_dim is not None: - self.news_proj = nn.Linear(self.news_dim, self.d_model) - else: - self.news_proj = None - - self.apply(self._init_weights) - - def _init_weights(self, module): - - if isinstance(module, nn.Linear): - nn.init.xavier_normal_(module.weight) - if module.bias is not None: - nn.init.zeros_(module.bias) - elif isinstance(module, nn.Embedding): - nn.init.normal_(module.weight, mean=0, std=self.embedding.d_model ** -0.5) - elif isinstance(module, nn.LayerNorm): - nn.init.ones_(module.weight) - nn.init.zeros_(module.bias) - elif isinstance(module, RMSNorm): - nn.init.ones_(module.weight) - - def forward(self, s1_ids, s2_ids, stamp=None, padding_mask=None, use_teacher_forcing=False, s1_targets=None, news_emb=None): - """ - Args: - s1_ids (torch.Tensor): Input tensor of s1 token IDs. Shape: [batch_size, seq_len] - s2_ids (torch.Tensor): Input tensor of s2 token IDs. Shape: [batch_size, seq_len] - stamp (torch.Tensor, optional): Temporal stamp tensor. Shape: [batch_size, seq_len]. Defaults to None. - padding_mask (torch.Tensor, optional): Mask for padding tokens. Shape: [batch_size, seq_len]. Defaults to None. - use_teacher_forcing (bool, optional): Whether to use teacher forcing for s1 decoding. Defaults to False. - s1_targets (torch.Tensor, optional): Target s1 token IDs for teacher forcing. Shape: [batch_size, seq_len]. Defaults to None. - news_emb (torch.Tensor, optional): News embedding tensor. Shape: [batch_size, news_dim]. Defaults to None. - - Returns: - Tuple[torch.Tensor, torch.Tensor]: - - s1 logits: Logits for s1 token predictions. Shape: [batch_size, seq_len, s1_vocab_size] - - s2_logits: Logits for s2 token predictions, conditioned on s1. Shape: [batch_size, seq_len, s2_vocab_size] - """ - x = self.embedding([s1_ids, s2_ids]) - if stamp is not None: - time_embedding = self.time_emb(stamp) - x = x + time_embedding - x = self.token_drop(x) - - for layer in self.transformer: - x = layer(x, key_padding_mask=padding_mask) - - x = self.norm(x) - - if news_emb is not None and self.news_proj is not None: - news_bias = self.news_proj(news_emb).unsqueeze(1) # [B, 1, d_model] - x = x + news_bias - - s1_logits = self.head(x) - - if use_teacher_forcing: - sibling_embed = self.embedding.emb_s1(s1_targets) - else: - s1_probs = F.softmax(s1_logits.detach(), dim=-1) - sample_s1_ids = torch.multinomial(s1_probs.view(-1, self.s1_vocab_size), 1).view(s1_ids.shape) - sibling_embed = self.embedding.emb_s1(sample_s1_ids) - - x2 = self.dep_layer(x, sibling_embed, key_padding_mask=padding_mask) # Dependency Aware Layer: Condition on s1 embeddings - s2_logits = self.head.cond_forward(x2) - return s1_logits, s2_logits - - def decode_s1(self, s1_ids, s2_ids, stamp=None, padding_mask=None, news_emb=None): - """ - Decodes only the s1 tokens. - - This method performs a forward pass to predict only s1 tokens. It returns the s1 logits - and the context representation from the Transformer, which can be used for subsequent s2 decoding. - - Args: - s1_ids (torch.Tensor): Input tensor of s1 token IDs. Shape: [batch_size, seq_len] - s2_ids (torch.Tensor): Input tensor of s2 token IDs. Shape: [batch_size, seq_len] - stamp (torch.Tensor, optional): Temporal stamp tensor. Shape: [batch_size, seq_len]. Defaults to None. - padding_mask (torch.Tensor, optional): Mask for padding tokens. Shape: [batch_size, seq_len]. Defaults to None. - news_emb (torch.Tensor, optional): News embedding tensor. Shape: [batch_size, news_dim]. Defaults to None. - - Returns: - Tuple[torch.Tensor, torch.Tensor]: - - s1 logits: Logits for s1 token predictions. Shape: [batch_size, seq_len, s1_vocab_size] - - context: Context representation from the Transformer. Shape: [batch_size, seq_len, d_model] - """ - x = self.embedding([s1_ids, s2_ids]) - if stamp is not None: - time_embedding = self.time_emb(stamp) - x = x + time_embedding - x = self.token_drop(x) - - for layer in self.transformer: - x = layer(x, key_padding_mask=padding_mask) - - x = self.norm(x) - - if news_emb is not None and self.news_proj is not None: - news_bias = self.news_proj(news_emb).unsqueeze(1) # [B, 1, d_model] - x = x + news_bias - - s1_logits = self.head(x) - return s1_logits, x - - def decode_s2(self, context, s1_ids, padding_mask=None): - """ - Decodes the s2 tokens, conditioned on the context and s1 tokens. - - This method decodes s2 tokens based on a pre-computed context representation (typically from `decode_s1`) - and the s1 token IDs. It uses the dependency-aware layer and the conditional s2 head to predict s2 tokens. - - Args: - context (torch.Tensor): Context representation from the transformer (output of decode_s1). - Shape: [batch_size, seq_len, d_model] - s1_ids (torch.torch.Tensor): Input tensor of s1 token IDs. Shape: [batch_size, seq_len] - padding_mask (torch.Tensor, optional): Mask for padding tokens. Shape: [batch_size, seq_len]. Defaults to None. - - Returns: - torch.Tensor: s2 logits. Shape: [batch_size, seq_len, s2_vocab_size] - """ - sibling_embed = self.embedding.emb_s1(s1_ids) - x2 = self.dep_layer(context, sibling_embed, key_padding_mask=padding_mask) - return self.head.cond_forward(x2) - - -def top_k_top_p_filtering( - logits, - top_k: int = 0, - top_p: float = 1.0, - filter_value: float = -float("Inf"), - min_tokens_to_keep: int = 1, -): - """Filter a distribution of logits using top-k and/or nucleus (top-p) filtering - Args: - logits: logits distribution shape (batch size, vocabulary size) - if top_k > 0: keep only top k tokens with highest probability (top-k filtering). - if top_p < 1.0: keep the top tokens with cumulative probability >= top_p (nucleus filtering). - Nucleus filtering is described in Holtzman et al. (http://arxiv.org/abs/1904.09751) - Make sure we keep at least min_tokens_to_keep per batch example in the output - From: https://gist.github.com/thomwolf/1a5a29f6962089e871b94cbd09daf317 - """ - if top_k > 0: - top_k = min(max(top_k, min_tokens_to_keep), logits.size(-1)) # Safety check - # Remove all tokens with a probability less than the last token of the top-k - indices_to_remove = logits < torch.topk(logits, top_k)[0][..., -1, None] - logits[indices_to_remove] = filter_value - return logits - - if top_p < 1.0: - sorted_logits, sorted_indices = torch.sort(logits, descending=True) - cumulative_probs = torch.cumsum(F.softmax(sorted_logits, dim=-1), dim=-1) - - # Remove tokens with cumulative probability above the threshold (token with 0 are kept) - sorted_indices_to_remove = cumulative_probs > top_p - if min_tokens_to_keep > 1: - # Keep at least min_tokens_to_keep (set to min_tokens_to_keep-1 because we add the first one below) - sorted_indices_to_remove[..., :min_tokens_to_keep] = 0 - # Shift the indices to the right to keep also the first token above the threshold - sorted_indices_to_remove[..., 1:] = sorted_indices_to_remove[..., :-1].clone() - sorted_indices_to_remove[..., 0] = 0 - - # scatter sorted tensors to original indexing - indices_to_remove = sorted_indices_to_remove.scatter(1, sorted_indices, sorted_indices_to_remove) - logits[indices_to_remove] = filter_value - return logits - - -def sample_from_logits(logits, temperature=1.0, top_k=None, top_p=None, sample_logits=True): - logits = logits / temperature - if top_k is not None or top_p is not None: - if top_k > 0 or top_p < 1.0: - logits = top_k_top_p_filtering(logits, top_k=top_k, top_p=top_p) - - probs = F.softmax(logits, dim=-1) - - if not sample_logits: - _, x = top_k(probs, k=1, dim=-1) - else: - x = torch.multinomial(probs, num_samples=1) - - return x - - -def auto_regressive_inference(tokenizer, model, x, x_stamp, y_stamp, max_context, pred_len, clip=5, T=1.0, top_k=0, top_p=0.99, sample_count=5, verbose=False, news_emb=None): - with torch.no_grad(): - x = torch.clip(x, -clip, clip) - - device = x.device - x = x.unsqueeze(1).repeat(1, sample_count, 1, 1).reshape(-1, x.size(1), x.size(2)).to(device) - x_stamp = x_stamp.unsqueeze(1).repeat(1, sample_count, 1, 1).reshape(-1, x_stamp.size(1), x_stamp.size(2)).to(device) - y_stamp = y_stamp.unsqueeze(1).repeat(1, sample_count, 1, 1).reshape(-1, y_stamp.size(1), y_stamp.size(2)).to(device) - - x_token = tokenizer.encode(x, half=True) - - initial_seq_len = x.size(1) - batch_size = x_token[0].size(0) - total_seq_len = initial_seq_len + pred_len - full_stamp = torch.cat([x_stamp, y_stamp], dim=1) - - generated_pre = x_token[0].new_empty(batch_size, pred_len) - generated_post = x_token[1].new_empty(batch_size, pred_len) - - pre_buffer = x_token[0].new_zeros(batch_size, max_context) - post_buffer = x_token[1].new_zeros(batch_size, max_context) - buffer_len = min(initial_seq_len, max_context) - if buffer_len > 0: - start_idx = max(0, initial_seq_len - max_context) - pre_buffer[:, :buffer_len] = x_token[0][:, start_idx:start_idx + buffer_len] - post_buffer[:, :buffer_len] = x_token[1][:, start_idx:start_idx + buffer_len] - - if verbose: - ran = trange - else: - ran = range - for i in ran(pred_len): - current_seq_len = initial_seq_len + i - window_len = min(current_seq_len, max_context) - - if current_seq_len <= max_context: - input_tokens = [ - pre_buffer[:, :window_len], - post_buffer[:, :window_len] - ] - else: - input_tokens = [pre_buffer, post_buffer] - - context_end = current_seq_len - context_start = max(0, context_end - max_context) - current_stamp = full_stamp[:, context_start:context_end, :].contiguous() - - s1_logits, context = model.decode_s1(input_tokens[0], input_tokens[1], current_stamp, news_emb=news_emb) - s1_logits = s1_logits[:, -1, :] - sample_pre = sample_from_logits(s1_logits, temperature=T, top_k=top_k, top_p=top_p, sample_logits=True) - - s2_logits = model.decode_s2(context, sample_pre) - s2_logits = s2_logits[:, -1, :] - sample_post = sample_from_logits(s2_logits, temperature=T, top_k=top_k, top_p=top_p, sample_logits=True) - - generated_pre[:, i] = sample_pre.squeeze(-1) - generated_post[:, i] = sample_post.squeeze(-1) - - if current_seq_len < max_context: - pre_buffer[:, current_seq_len] = sample_pre.squeeze(-1) - post_buffer[:, current_seq_len] = sample_post.squeeze(-1) - else: - pre_buffer.copy_(torch.roll(pre_buffer, shifts=-1, dims=1)) - post_buffer.copy_(torch.roll(post_buffer, shifts=-1, dims=1)) - pre_buffer[:, -1] = sample_pre.squeeze(-1) - post_buffer[:, -1] = sample_post.squeeze(-1) - - full_pre = torch.cat([x_token[0], generated_pre], dim=1) - full_post = torch.cat([x_token[1], generated_post], dim=1) - - context_start = max(0, total_seq_len - max_context) - input_tokens = [ - full_pre[:, context_start:total_seq_len].contiguous(), - full_post[:, context_start:total_seq_len].contiguous() - ] - z = tokenizer.decode(input_tokens, half=True) - z = z.reshape(-1, sample_count, z.size(1), z.size(2)) - preds = z.cpu().numpy() - preds = np.mean(preds, axis=1) - - return preds - - -def calc_time_stamps(x_timestamp): - time_df = pd.DataFrame() - time_df['minute'] = x_timestamp.dt.minute - time_df['hour'] = x_timestamp.dt.hour - time_df['weekday'] = x_timestamp.dt.weekday - time_df['day'] = x_timestamp.dt.day - time_df['month'] = x_timestamp.dt.month - return time_df - - -class KronosPredictor: - - def __init__(self, model, tokenizer, device="cuda:0", max_context=512, clip=5): - self.tokenizer = tokenizer - self.model = model - self.max_context = max_context - self.clip = clip - self.price_cols = ['open', 'high', 'low', 'close'] - self.vol_col = 'volume' - self.amt_vol = 'amount' - self.time_cols = ['minute', 'hour', 'weekday', 'day', 'month'] - self.device = device - - self.tokenizer = self.tokenizer.to(self.device) - self.model = self.model.to(self.device) - - def generate(self, x, x_stamp, y_stamp, pred_len, T, top_k, top_p, sample_count, verbose, news_emb=None): - - x_tensor = torch.from_numpy(np.array(x).astype(np.float32)).to(self.device) - x_stamp_tensor = torch.from_numpy(np.array(x_stamp).astype(np.float32)).to(self.device) - y_stamp_tensor = torch.from_numpy(np.array(y_stamp).astype(np.float32)).to(self.device) - - preds = auto_regressive_inference(self.tokenizer, self.model, x_tensor, x_stamp_tensor, y_stamp_tensor, self.max_context, pred_len, - self.clip, T, top_k, top_p, sample_count, verbose, news_emb=news_emb) - preds = preds[:, -pred_len:, :] - return preds - - def predict(self, df, x_timestamp, y_timestamp, pred_len, T=1.0, top_k=0, top_p=0.9, sample_count=1, verbose=True, news_emb=None): - - if not isinstance(df, pd.DataFrame): - raise ValueError("Input must be a pandas DataFrame.") - - if not all(col in df.columns for col in self.price_cols): - raise ValueError(f"Price columns {self.price_cols} not found in DataFrame.") - - df = df.copy() - if self.vol_col not in df.columns: - df[self.vol_col] = 0.0 # Fill missing volume with zeros - df[self.amt_vol] = 0.0 # Fill missing amount with zeros - if self.amt_vol not in df.columns and self.vol_col in df.columns: - df[self.amt_vol] = df[self.vol_col] * df[self.price_cols].mean(axis=1) - - if df[self.price_cols + [self.vol_col, self.amt_vol]].isnull().values.any(): - raise ValueError("Input DataFrame contains NaN values in price or volume columns.") - - x_time_df = calc_time_stamps(x_timestamp) - y_time_df = calc_time_stamps(y_timestamp) - - x = df[self.price_cols + [self.vol_col, self.amt_vol]].values.astype(np.float32) - x_stamp = x_time_df.values.astype(np.float32) - y_stamp = y_time_df.values.astype(np.float32) - - x_mean, x_std = np.mean(x, axis=0), np.std(x, axis=0) - - x = (x - x_mean) / (x_std + 1e-5) - x = np.clip(x, -self.clip, self.clip) - - x = x[np.newaxis, :] - x_stamp = x_stamp[np.newaxis, :] - y_stamp = y_stamp[np.newaxis, :] - - if news_emb is not None: - news_emb_tensor = torch.from_numpy(np.array(news_emb).astype(np.float32)).to(self.device) - # Ensure batch dimension for news_emb if only one sample - if news_emb_tensor.ndim == 1: - news_emb_tensor = news_emb_tensor.unsqueeze(0) - else: - news_emb_tensor = None - - preds = self.generate(x, x_stamp, y_stamp, pred_len, T, top_k, top_p, sample_count, verbose, news_emb=news_emb_tensor) - - preds = preds.squeeze(0) - preds = preds * (x_std + 1e-5) + x_mean - - pred_df = pd.DataFrame(preds, columns=self.price_cols + [self.vol_col, self.amt_vol], index=y_timestamp) - return pred_df - - - def predict_batch(self, df_list, x_timestamp_list, y_timestamp_list, pred_len, T=1.0, top_k=0, top_p=0.9, sample_count=1, verbose=True): - """ - Perform parallel (batch) prediction on multiple time series. All series must have the same historical length and prediction length (pred_len). - - Args: - df_list (List[pd.DataFrame]): List of input DataFrames, each containing price columns and optional volume/amount columns. - x_timestamp_list (List[pd.DatetimeIndex or Series]): List of timestamps corresponding to historical data, length should match the number of rows in each DataFrame. - y_timestamp_list (List[pd.DatetimeIndex or Series]): List of future prediction timestamps, length should equal pred_len. - pred_len (int): Number of prediction steps. - T (float): Sampling temperature. - top_k (int): Top-k filtering threshold. - top_p (float): Top-p (nucleus sampling) threshold. - sample_count (int): Number of parallel samples per series, automatically averaged internally. - verbose (bool): Whether to display autoregressive progress. - - Returns: - List[pd.DataFrame]: List of prediction results in the same order as input, each DataFrame contains - `open, high, low, close, volume, amount` columns, indexed by corresponding `y_timestamp`. - """ - # Basic validation - if not isinstance(df_list, (list, tuple)) or not isinstance(x_timestamp_list, (list, tuple)) or not isinstance(y_timestamp_list, (list, tuple)): - raise ValueError("df_list, x_timestamp_list, y_timestamp_list must be list or tuple types.") - if not (len(df_list) == len(x_timestamp_list) == len(y_timestamp_list)): - raise ValueError("df_list, x_timestamp_list, y_timestamp_list must have consistent lengths.") - - num_series = len(df_list) - - x_list = [] - x_stamp_list = [] - y_stamp_list = [] - means = [] - stds = [] - seq_lens = [] - y_lens = [] - - for i in range(num_series): - df = df_list[i] - if not isinstance(df, pd.DataFrame): - raise ValueError(f"Input at index {i} is not a pandas DataFrame.") - if not all(col in df.columns for col in self.price_cols): - raise ValueError(f"DataFrame at index {i} is missing price columns {self.price_cols}.") - - df = df.copy() - if self.vol_col not in df.columns: - df[self.vol_col] = 0.0 - df[self.amt_vol] = 0.0 - if self.amt_vol not in df.columns and self.vol_col in df.columns: - df[self.amt_vol] = df[self.vol_col] * df[self.price_cols].mean(axis=1) - - if df[self.price_cols + [self.vol_col, self.amt_vol]].isnull().values.any(): - raise ValueError(f"DataFrame at index {i} contains NaN values in price or volume columns.") - - x_timestamp = x_timestamp_list[i] - y_timestamp = y_timestamp_list[i] - - x_time_df = calc_time_stamps(x_timestamp) - y_time_df = calc_time_stamps(y_timestamp) - - x = df[self.price_cols + [self.vol_col, self.amt_vol]].values.astype(np.float32) - x_stamp = x_time_df.values.astype(np.float32) - y_stamp = y_time_df.values.astype(np.float32) - - if x.shape[0] != x_stamp.shape[0]: - raise ValueError(f"Inconsistent lengths at index {i}: x has {x.shape[0]} vs x_stamp has {x_stamp.shape[0]}.") - if y_stamp.shape[0] != pred_len: - raise ValueError(f"y_timestamp length at index {i} should equal pred_len={pred_len}, got {y_stamp.shape[0]}.") - - x_mean, x_std = np.mean(x, axis=0), np.std(x, axis=0) - x_norm = (x - x_mean) / (x_std + 1e-5) - x_norm = np.clip(x_norm, -self.clip, self.clip) - - x_list.append(x_norm) - x_stamp_list.append(x_stamp) - y_stamp_list.append(y_stamp) - means.append(x_mean) - stds.append(x_std) - - seq_lens.append(x_norm.shape[0]) - y_lens.append(y_stamp.shape[0]) - - # Require all series to have consistent historical and prediction lengths for batch processing - if len(set(seq_lens)) != 1: - raise ValueError(f"Parallel prediction requires all series to have consistent historical lengths, got: {seq_lens}") - if len(set(y_lens)) != 1: - raise ValueError(f"Parallel prediction requires all series to have consistent prediction lengths, got: {y_lens}") - - x_batch = np.stack(x_list, axis=0).astype(np.float32) # (B, seq_len, feat) - x_stamp_batch = np.stack(x_stamp_list, axis=0).astype(np.float32) # (B, seq_len, time_feat) - y_stamp_batch = np.stack(y_stamp_list, axis=0).astype(np.float32) # (B, pred_len, time_feat) - - preds = self.generate(x_batch, x_stamp_batch, y_stamp_batch, pred_len, T, top_k, top_p, sample_count, verbose) - # preds: (B, pred_len, feat) - - pred_dfs = [] - for i in range(num_series): - preds_i = preds[i] * (stds[i] + 1e-5) + means[i] - pred_df = pd.DataFrame(preds_i, columns=self.price_cols + [self.vol_col, self.amt_vol], index=y_timestamp_list[i]) - pred_dfs.append(pred_df) - - return pred_dfs diff --git a/skills/alphaear-reporter/scripts/utils/predictor/model/module.py b/skills/alphaear-reporter/scripts/utils/predictor/model/module.py deleted file mode 100644 index 20b29b5..0000000 --- a/skills/alphaear-reporter/scripts/utils/predictor/model/module.py +++ /dev/null @@ -1,562 +0,0 @@ -import math - -from einops import rearrange, reduce -import torch -import torch.nn as nn -from torch.autograd import Function -import torch.nn.functional as F - - -class DifferentiableEntropyFunction(Function): - @staticmethod - def forward(ctx, zq, basis, K, eps): - zb = (zq + 1) / 2 - zi = ((zb * basis).sum(-1)).to(torch.int64) - cnt = torch.scatter_reduce(torch.zeros(2 ** K, device=zq.device, dtype=zq.dtype), - 0, - zi.flatten(), - torch.ones_like(zi.flatten()).to(zq.dtype), - 'sum') - prob = (cnt + eps) / (cnt + eps).sum() - H = -(prob * torch.log(prob)).sum() - ctx.save_for_backward(zq, zi, prob) - ctx.K = K - return H - - @staticmethod - def backward(ctx, grad_output): - zq, zi, prob = ctx.saved_tensors - grad_array = -grad_output * (torch.log(prob) + 1) / zi.numel() / ctx.K - reord_grad = grad_array[zi.flatten()].reshape(zi.shape) - grad_input = reord_grad.unsqueeze(-1) * zq - return grad_input, None, None, None, None - - -def codebook_entropy(zq, basis, K, eps=1e-4): - return DifferentiableEntropyFunction.apply(zq, basis, K, eps) - - -class BinarySphericalQuantizer(nn.Module): - def __init__(self, embed_dim, beta, gamma0, gamma, zeta, - input_format='bchw', - soft_entropy=True, group_size=9, - persample_entropy_compute='analytical', - cb_entropy_compute='group', - l2_norm=True, - inv_temperature=1): - """ - Paper link: https://arxiv.org/pdf/2406.07548.pdf - Here we use the official implementation of the BinarySphericalQuantizer. - """ - super().__init__() - self.embed_dim = embed_dim - self.beta = beta # loss weight for commit loss - self.gamma0 = gamma0 # loss weight for entropy penalty - self.gamma = gamma # loss weight for entropy penalty - self.zeta = zeta # loss weight for entire entropy penalty - self.input_format = input_format - assert self.embed_dim % group_size == 0, "embed_dim must be divisible by group_size" - self.num_groups = self.embed_dim // group_size - self.group_size = group_size - assert persample_entropy_compute in ['group', 'analytical'], "persample_entropy_compute must be either 'group' or 'analytical'" - assert cb_entropy_compute in ['group', 'nce'], "cb_entropy_compute must be either 'group' or 'nce'" - self.persample_entropy_compute = persample_entropy_compute - self.cb_entropy_compute = cb_entropy_compute - self.l2_norm = l2_norm - self.inv_temperature = inv_temperature - - self.register_buffer('basis', 2 ** torch.arange(embed_dim - 1, -1, -1)) - self.register_buffer('group_basis', 2 ** torch.arange(group_size - 1, -1, -1)) - - self.num_dimensions = 2 ** embed_dim - self.bits_per_index = embed_dim - - # we only need to keep the codebook portion up to the group size - # because we approximate the H loss with this subcode - group_codes = torch.arange(2 ** self.group_size) - group_codebook = self.indexes_to_codes(group_codes).float()[:, -group_size:] - self.register_buffer('group_codebook', group_codebook, persistent=False) - - self.soft_entropy = soft_entropy # soft_entropy: Sec 3.2 of https://arxiv.org/pdf/1911.05894.pdf - - def quantize(self, z): - assert z.shape[-1] == self.embed_dim, f"Expected {self.embed_dim} dimensions, got {z.shape[-1]}" - - zhat = torch.where(z > 0, - torch.tensor(1, dtype=z.dtype, device=z.device), - torch.tensor(-1, dtype=z.dtype, device=z.device)) - return z + (zhat - z).detach() - - def forward(self, z, collect_metrics=True): - # if self.input_format == 'bchw': - # z = rearrange(z, 'b c h w -> b h w c') - zq = self.quantize(z) - - q_scale = 1. / (self.embed_dim ** 0.5) if self.l2_norm else 1. - - zq = zq * q_scale - - if not collect_metrics: - return zq, zq.new_zeros(()), {} - - indices = self.codes_to_indexes(zq.detach()) - group_indices = self.codes_to_group_indexes(zq.detach()) - if not self.training: - used_codes = torch.unique(indices, return_counts=False) - else: - used_codes = None - - if self.soft_entropy: - persample_entropy, cb_entropy, avg_prob = self.soft_entropy_loss(z) - entropy_penalty = self.gamma0 * persample_entropy - self.gamma * cb_entropy - else: - zb_by_sample = ((zq + 1) / 2).reshape(z.shape[0], -1, z.shape[-1]).to(torch.float32) - persample_entropy = self.get_hard_per_sample_entropy(zb_by_sample) - cb_entropy = codebook_entropy(zq, self.basis, self.embed_dim) - entropy_penalty = self.gamma0 * persample_entropy - self.gamma * cb_entropy - - # commit loss - commit_loss = self.beta * torch.mean(((zq.detach() - z) ** 2).sum(dim=-1)) - - # if self.input_format == 'bchw': - # zq = rearrange(zq, 'b h w c -> b c h w') - - return ( - zq, - commit_loss + self.zeta * entropy_penalty / self.inv_temperature, - {"H": cb_entropy, "used_codes": used_codes, "indices": indices, "group_indices": group_indices, - "avg_prob": avg_prob} - ) - - def soft_entropy_loss(self, z): - # if we divide the code in subgroups of size group_size, the codebook will be of size 2 ** group_size - # the sub-code is the last group_size bits of the full code - group_code_book = self.group_codebook / (self.embed_dim ** 0.5 if self.l2_norm else 1) - divided_z = rearrange(z, '... (g c) -> ... g c', c=self.group_size) - - # we calculate the distance between the divided_z and the codebook for each subgroup - distance = - 2 * torch.einsum('... g c, d c ->... g d', divided_z, group_code_book) - prob = (-distance * self.inv_temperature).softmax(dim=-1) - if self.persample_entropy_compute == 'analytical': - if self.l2_norm: - p = torch.sigmoid(-4 * z / (self.embed_dim ** 0.5) * self.inv_temperature) - else: - p = torch.sigmoid(-4 * z * self.inv_temperature) - prob = torch.stack([p, 1 - p], dim=-1) - per_sample_entropy = self.get_entropy(prob, dim=-1, normalize=False).sum(dim=-1).mean() - else: - per_sample_entropy = self.get_entropy(prob, dim=-1, normalize=False).sum(dim=-1).mean() - - # macro average of the probability of each subgroup - avg_prob = reduce(prob, '... g d ->g d', 'mean') - codebook_entropy = self.get_entropy(avg_prob, dim=-1, normalize=False) - - # the approximation of the entropy is the sum of the entropy of each subgroup - return per_sample_entropy, codebook_entropy.sum(), avg_prob - - def get_hard_per_sample_entropy(self, zb_by_sample): - probs_per_dim = zb_by_sample.sum(1) / zb_by_sample.shape[1] - persample_entropy = - probs_per_dim * torch.log(probs_per_dim + 1e-8) - (1 - probs_per_dim) * torch.log(1 - probs_per_dim + 1e-8) - persample_entropy = persample_entropy.sum(-1) - return persample_entropy.mean() - - def codes_to_indexes(self, zhat): - """Converts a `code` to an index in the codebook. - Args: - zhat: A tensor of shape (B, ..., C) containing the codes. must be in {-1, 1} - """ - assert zhat.shape[-1] == self.embed_dim, f"Expected {self.embed_dim} dimensions, got {zhat.shape[-1]}" - return ((zhat + 1) / 2 * self.basis).sum(axis=-1).to(torch.int64) - - def codes_to_group_indexes(self, zhat): - """Converts a `code` to a list of indexes (in groups) in the codebook. - Args: - zhat: A tensor of shape (B, ..., C) containing the codes. must be in {-1, 1} - """ - zhat_in_group = rearrange(zhat, 'b ... (g c) -> b ... g c', c=self.group_size) - return ((zhat_in_group + 1) / 2 * self.group_basis).sum(axis=-1).to(torch.int64) - - def indexes_to_codes(self, indices): - """Inverse of `indexes_to_codes`.""" - indices = indices.unsqueeze(-1) - codes_non_centered = torch.remainder( - torch.floor_divide(indices, self.basis), 2 - ) - return codes_non_centered * 2 - 1 - - def group_indexes_to_codes(self, group_indices): - """Inverse of `group_indexes_to_codes`.""" - group_indices = group_indices.unsqueeze(-1) - codes_non_centered = torch.remainder( - torch.floor_divide(group_indices, self.group_basis), 2 - ) - codes_non_centered = rearrange(codes_non_centered, 'b ... g c -> b ... (g c)') - return codes_non_centered * 2 - 1 - - def get_entropy(self, count, dim=-1, eps=1e-4, normalize=True): - if normalize: - probs = (count + eps) / (count + eps).sum(dim=dim, keepdim=True) - else: - probs = count - H = -(probs * torch.log(probs + 1e-8)).sum(dim=dim) - return H - - def get_group_codebook_entry(self, group_indices): - z_q = self.group_indexes_to_codes(group_indices) - q_scale = 1. / (self.embed_dim ** 0.5) if self.l2_norm else 1. - z_q = z_q * q_scale - if self.input_format == 'bchw': - h, w = int(z_q.shape[1] ** 0.5) - assert h * w == z_q.shape[1], 'Invalid sequence length' - z_q = rearrange(z_q, 'b (h w) c -> b c h w', h=h) - return z_q - - def get_codebook_entry(self, indices): - z_q = self.indexes_to_codes(indices) - q_scale = 1. / (self.embed_dim ** 0.5) if self.l2_norm else 1. - z_q = z_q * q_scale - if self.input_format == 'bchw': - h, w = int(z_q.shape[1] ** 0.5) - assert h * w == z_q.shape[1], 'Invalid sequence length' - z_q = rearrange(z_q, 'b (h w) c -> b c h w', h=h) - return z_q - - -class BSQuantizer(nn.Module): - - def __init__(self, s1_bits, s2_bits, beta, gamma0, gamma, zeta, group_size): - super().__init__() - self.codebook_dim = s1_bits + s2_bits - self.s1_bits = s1_bits - self.s2_bits = s2_bits - self.bsq = BinarySphericalQuantizer(self.codebook_dim, beta, gamma0, gamma, zeta, group_size=group_size) - - def bits_to_indices(self, bits): - bits = (bits >= 0).to(torch.long) - indices = 2 ** torch.arange( - 0, - bits.shape[-1], - 1, - dtype=torch.long, - device=bits.device, - ) - return (bits * indices).sum(-1) - - def forward(self, z, half=False, collect_metrics=True): - z = F.normalize(z, dim=-1) - quantized, bsq_loss, metrics = self.bsq(z, collect_metrics=collect_metrics) - if half: - q_pre = quantized[:, :, :self.s1_bits] - q_post = quantized[:, :, self.s1_bits:] - z_indices = [self.bits_to_indices(q_pre), self.bits_to_indices(q_post)] - else: - z_indices = self.bits_to_indices(quantized) - return bsq_loss, quantized, z_indices - - -class RMSNorm(torch.nn.Module): - def __init__(self, dim: int, eps: float = 1e-5): - super().__init__() - self.eps = eps - self.weight = nn.Parameter(torch.ones(dim)) - - def _norm(self, x): - return x * torch.rsqrt(torch.mean(x * x, dim=-1, keepdim=True) + self.eps) - - def forward(self, x): - output = self._norm(x.float()).type_as(x) - return output * self.weight - - -class FeedForward(nn.Module): - def __init__(self, d_model, ff_dim, ffn_dropout_p=0.0): - super().__init__() - - self.w1 = nn.Linear(d_model, ff_dim, bias=False) - self.w3 = nn.Linear(d_model, ff_dim, bias=False) - self.w2 = nn.Linear(ff_dim, d_model, bias=False) - self.ffn_dropout = nn.Dropout(ffn_dropout_p) - - def forward(self, x): - return self.ffn_dropout(self.w2(F.silu(self.w1(x)) * self.w3(x))) - - -class RotaryPositionalEmbedding(nn.Module): - def __init__(self, dim): - super().__init__() - inv_freq = 1.0 / (10000 ** (torch.arange(0, dim, 2).float() / dim)) - self.register_buffer("inv_freq", inv_freq) - self.seq_len_cached = None - self.cos_cached = None - self.sin_cached = None - - def _update_cos_sin_cache(self, x, seq_len): - if seq_len != self.seq_len_cached: - self.seq_len_cached = seq_len - t = torch.arange(seq_len, device=x.device).type_as(self.inv_freq) - freqs = torch.einsum('i,j->ij', t, self.inv_freq) - emb = torch.cat((freqs, freqs), dim=-1).to(x.device) - self.cos_cached = emb.cos()[None, None, :, :] - self.sin_cached = emb.sin()[None, None, :, :] - return self.cos_cached, self.sin_cached - - def forward(self, q, k): - cos, sin = self._update_cos_sin_cache(q, q.shape[-2]) - return ( - (q * cos) + (self._rotate_half(q) * sin), - (k * cos) + (self._rotate_half(k) * sin), - ) - - def _rotate_half(self, x): - x1, x2 = x.chunk(2, dim=-1) - return torch.cat((-x2, x1), dim=-1) - - -class MultiHeadAttentionWithRoPE(nn.Module): - def __init__(self, d_model, n_heads, attn_dropout_p=0.0, resid_dropout_p=0.0): - super().__init__() - self.d_model = d_model - self.n_heads = n_heads - self.head_dim = d_model // n_heads - - self.q_proj = nn.Linear(d_model, d_model) - self.k_proj = nn.Linear(d_model, d_model) - self.v_proj = nn.Linear(d_model, d_model) - self.out_proj = nn.Linear(d_model, d_model) - self.rotary = RotaryPositionalEmbedding(self.head_dim) - self.attn_dropout_p = attn_dropout_p - self.resid_dropout = nn.Dropout(resid_dropout_p) - - def forward(self, x, key_padding_mask=None): - batch_size, seq_len, _ = x.shape - - q = self.q_proj(x).view(batch_size, seq_len, self.n_heads, self.head_dim).transpose(1, 2) - k = self.k_proj(x).view(batch_size, seq_len, self.n_heads, self.head_dim).transpose(1, 2) - v = self.v_proj(x).view(batch_size, seq_len, self.n_heads, self.head_dim).transpose(1, 2) - - q, k = self.rotary(q, k) - - if key_padding_mask is not None: - attn_mask = key_padding_mask.unsqueeze(1).unsqueeze(2) # [batch, 1, 1, seq_len] - attn_mask = attn_mask.expand(-1, self.n_heads, seq_len, -1) # [batch, n_heads, q_len, k_len] - else: - attn_mask = None - - attn_output = F.scaled_dot_product_attention( - q, k, v, - attn_mask=attn_mask, - dropout_p=self.attn_dropout_p if self.training else 0.0, - is_causal=True - ) - - attn_output = attn_output.transpose(1, 2).contiguous().view(batch_size, seq_len, self.d_model) - return self.resid_dropout(self.out_proj(attn_output)) - - -class MultiHeadCrossAttentionWithRoPE(nn.Module): - def __init__(self, d_model, n_heads, attn_dropout_p=0.0, resid_dropout=0.0): - super().__init__() - self.d_model = d_model - self.n_heads = n_heads - self.head_dim = d_model // n_heads - - self.q_proj = nn.Linear(d_model, d_model) - self.k_proj = nn.Linear(d_model, d_model) - self.v_proj = nn.Linear(d_model, d_model) - self.out_proj = nn.Linear(d_model, d_model) - self.rotary = RotaryPositionalEmbedding(self.head_dim) - self.attn_dropout_p = attn_dropout_p - self.resid_dropout = nn.Dropout(resid_dropout) - - def forward(self, query, key, value, key_padding_mask=None): - batch_size, q_len, _ = query.shape - _, seq_len, _ = key.shape - - q = self.q_proj(query).view(batch_size, q_len, self.n_heads, self.head_dim).transpose(1, 2) - k = self.k_proj(key).view(batch_size, seq_len, self.n_heads, self.head_dim).transpose(1, 2) - v = self.v_proj(value).view(batch_size, seq_len, self.n_heads, self.head_dim).transpose(1, 2) - - q, k = self.rotary(q, k) - - if key_padding_mask is not None: - attn_mask = key_padding_mask.unsqueeze(1).unsqueeze(2) - attn_mask = attn_mask.expand(-1, self.n_heads, q_len, -1) - else: - attn_mask = None - - is_causal_flag = self.training - - attn_output = F.scaled_dot_product_attention( - q, k, v, - attn_mask=attn_mask, - dropout_p=self.attn_dropout_p if self.training else 0.0, - is_causal=is_causal_flag - ) - - attn_output = attn_output.transpose(1, 2).contiguous().view(batch_size, q_len, self.d_model) - return self.resid_dropout(self.out_proj(attn_output)) - - -class HierarchicalEmbedding(nn.Module): - def __init__(self, s1_bits, s2_bits, d_model=256): - super().__init__() - self.s1_bits = s1_bits - self.s2_bits = s2_bits - - vocab_s1 = 2 ** s1_bits - vocab_s2 = 2 ** s2_bits - - self.emb_s1 = nn.Embedding(vocab_s1, d_model) - self.emb_s2 = nn.Embedding(vocab_s2, d_model) - self.d_model = d_model - self.fusion_proj = nn.Linear(d_model * 2, d_model) - - nn.init.normal_(self.emb_s1.weight, mean=0, std=d_model ** -0.5) - nn.init.normal_(self.emb_s2.weight, mean=0, std=d_model ** -0.5) - - def split_token(self, token_ids: torch.Tensor, s2_bits: int): - """Inputs: - token_ids (torch.Tensor): Composite token IDs of shape [batch_size, seq_len] or [N], each in range [0, 2^(s1_bits + s2_bits) - 1]. - s2_bits (int): Number of low bits used for the fine token (s2). - """ - assert isinstance(s2_bits, int) and s2_bits > 0, "s2_bits must be a positive integer" - - t = token_ids.long() - mask = (1 << s2_bits) - 1 - s2_ids = t & mask # extract low bits - s1_ids = t >> s2_bits # extract high bits - return s1_ids, s2_ids - - def forward(self, token_ids): - """Inputs: - token_ids: - - tuple or list: (s1_ids, s2_ids), each of shape [batch_size, seq_len], or - - torch.Tensor: composite token IDs of shape [batch_size, seq_len], which will be split into (s1_ids, s2_ids) internally. - Output: [batch_size, seq_len, d_model] - """ - if isinstance(token_ids, tuple) or isinstance(token_ids, list): - s1_ids, s2_ids = token_ids - else: - s1_ids, s2_ids = self.split_token(token_ids, self.s2_bits) - s1_emb = self.emb_s1(s1_ids) * math.sqrt(self.d_model) - s2_emb = self.emb_s2(s2_ids) * math.sqrt(self.d_model) - return self.fusion_proj(torch.cat([s1_emb, s2_emb], dim=-1)) - - -class DependencyAwareLayer(nn.Module): - def __init__(self, d_model, n_heads=4, attn_dropout_p=0.0, resid_dropout=0.0): - super().__init__() - self.cross_attn = MultiHeadCrossAttentionWithRoPE(d_model, n_heads, attn_dropout_p, resid_dropout) - self.norm = RMSNorm(d_model) - - def forward(self, hidden_states, sibling_embed, key_padding_mask=None): - """hidden_states: [batch, seq_len, d_model] - sibling_embed: Embedding from another subtoken - """ - attn_out = self.cross_attn( - query=sibling_embed, - key=hidden_states, - value=hidden_states, - key_padding_mask=key_padding_mask - ) - return self.norm(hidden_states + attn_out) - - -class TransformerBlock(nn.Module): - def __init__(self, d_model, n_heads, ff_dim=1024, ffn_dropout_p=0.0, attn_dropout_p=0.0, resid_dropout_p=0.0): - super().__init__() - self.norm1 = RMSNorm(d_model) - self.self_attn = MultiHeadAttentionWithRoPE(d_model, n_heads, attn_dropout_p, resid_dropout_p) - self.norm2 = RMSNorm(d_model) - self.ffn = FeedForward(d_model, ff_dim, ffn_dropout_p) - - def forward(self, x, key_padding_mask=None): - residual = x - x = self.norm1(x) - attn_out = self.self_attn(x, key_padding_mask=key_padding_mask) - x = residual + attn_out - - residual = x - x = self.norm2(x) - ffn_out = self.ffn(x) - x = residual + ffn_out - return x - - -class DualHead(nn.Module): - def __init__(self, s1_bits, s2_bits, d_model): - super().__init__() - self.vocab_s1 = 2 ** s1_bits - self.vocab_s2 = 2 ** s2_bits - self.proj_s1 = nn.Linear(d_model, self.vocab_s1) - self.proj_s2 = nn.Linear(d_model, self.vocab_s2) - - def compute_loss(self, s1_logits, s2_logits, s1_targets, s2_targets, padding_mask=None): - if padding_mask is not None: - valid_mask = (padding_mask == 0) - s1_logits = s1_logits[valid_mask] - s2_logits = s2_logits[valid_mask] - s1_targets = s1_targets[valid_mask] - s2_targets = s2_targets[valid_mask] - ce_s1 = F.cross_entropy(s1_logits, s1_targets) - ce_s2 = F.cross_entropy(s2_logits, s2_targets) - else: - ce_s1 = F.cross_entropy(s1_logits.reshape(-1, self.vocab_s1), s1_targets.reshape(-1)) - ce_s2 = F.cross_entropy(s2_logits.reshape(-1, self.vocab_s2), s2_targets.reshape(-1)) - ce_loss = (ce_s1 + ce_s2) / 2 - return ce_loss, ce_s1, ce_s2 - - def forward(self, x): - return self.proj_s1(x) - - def cond_forward(self, x2): - return self.proj_s2(x2) - - -class FixedEmbedding(nn.Module): - def __init__(self, c_in, d_model): - super(FixedEmbedding, self).__init__() - - w = torch.zeros(c_in, d_model).float() - w.require_grad = False - - position = torch.arange(0, c_in).float().unsqueeze(1) - div_term = (torch.arange(0, d_model, 2).float() * -(math.log(10000.0) / d_model)).exp() - - w[:, 0::2] = torch.sin(position * div_term) - w[:, 1::2] = torch.cos(position * div_term) - - self.emb = nn.Embedding(c_in, d_model) - self.emb.weight = nn.Parameter(w, requires_grad=False) - - def forward(self, x): - return self.emb(x).detach() - - -class TemporalEmbedding(nn.Module): - def __init__(self, d_model, learn_pe): - super(TemporalEmbedding, self).__init__() - - minute_size = 60 - hour_size = 24 - weekday_size = 7 - day_size = 32 - month_size = 13 - - Embed = FixedEmbedding if not learn_pe else nn.Embedding - self.minute_embed = Embed(minute_size, d_model) - self.hour_embed = Embed(hour_size, d_model) - self.weekday_embed = Embed(weekday_size, d_model) - self.day_embed = Embed(day_size, d_model) - self.month_embed = Embed(month_size, d_model) - - def forward(self, x): - x = x.long() - - minute_x = self.minute_embed(x[:, :, 0]) - hour_x = self.hour_embed(x[:, :, 1]) - weekday_x = self.weekday_embed(x[:, :, 2]) - day_x = self.day_embed(x[:, :, 3]) - month_x = self.month_embed(x[:, :, 4]) - - return hour_x + weekday_x + day_x + month_x + minute_x \ No newline at end of file diff --git a/skills/alphaear-reporter/scripts/utils/predictor/training.py b/skills/alphaear-reporter/scripts/utils/predictor/training.py deleted file mode 100644 index 3b41724..0000000 --- a/skills/alphaear-reporter/scripts/utils/predictor/training.py +++ /dev/null @@ -1,539 +0,0 @@ -import os -import sys -import time -import torch -import torch.nn as nn -import pandas as pd -import numpy as np -import json -import random -from loguru import logger -from datetime import datetime, timedelta -from sentence_transformers import SentenceTransformer -from skills._env_loader import load_unified_env - -load_unified_env() - -# Setup paths -KRONOS_DIR = os.path.dirname(os.path.abspath(__file__)) -SRC_DIR = os.path.dirname(os.path.dirname(KRONOS_DIR)) -if SRC_DIR not in sys.path: - sys.path.insert(0, SRC_DIR) - -from ..kronos.model import Kronos, KronosTokenizer, KronosPredictor -from ..database_manager import DatabaseManager -from ..stock_tools import StockTools -from ..search_tools import SearchTools -from ..llm.factory import get_model -from ..visualizer import VisualizerTools -from ..schema.models import ForecastResult, KLinePoint -from agno.agent import Agent - - -class AutoSynthesisTrainer: - def __init__(self, news_dim=384): - self.device = ( - "cuda" - if torch.cuda.is_available() - else "mps" - if torch.backends.mps.is_available() - else "cpu" - ) - self.db = DatabaseManager() - self.tools = StockTools(self.db) - self.searcher = SearchTools(self.db) - # Try loading from local cache first to avoid network timeouts - model_name = os.getenv( - "EMBEDDING_MODEL", "sentence-transformers/all-MiniLM-L6-v2" - ) - try: - logger.info(f"🔄 Attempting to load {model_name} from local cache...") - self.embedder = SentenceTransformer( - model_name, device=self.device, local_files_only=True - ) - logger.success("✅ Model loaded from local cache.") - except Exception: - logger.warning( - "⚠️ Local cache not found or incomplete. Attempting to download..." - ) - self.embedder = SentenceTransformer(model_name, device=self.device) - self.news_dim = news_dim - - # Try loading from local cache first to avoid network timeouts - try: - logger.info( - "🔄 Attempting to load Kronos and Tokenizer from local cache..." - ) - self.tokenizer = KronosTokenizer.from_pretrained( - "NeoQuasar/Kronos-Tokenizer-base", local_files_only=True - ).to(self.device) - base_model = Kronos.from_pretrained( - "NeoQuasar/Kronos-base", local_files_only=True - ) - logger.success("✅ Kronos and Tokenizer loaded from local cache.") - except Exception: - logger.warning( - "⚠️ Local Kronos/Tokenizer not found or incomplete. Attempting to download..." - ) - self.tokenizer = KronosTokenizer.from_pretrained( - "NeoQuasar/Kronos-Tokenizer-base" - ).to(self.device) - base_model = Kronos.from_pretrained("NeoQuasar/Kronos-base") - - self.model = Kronos( - base_model.s1_bits, - base_model.s2_bits, - base_model.n_layers, - base_model.d_model, - base_model.n_heads, - base_model.ff_dim, - base_model.ffn_dropout_p, - base_model.attn_dropout_p, - base_model.resid_dropout_p, - base_model.token_dropout_p, - base_model.learn_te, - news_dim=self.news_dim, - ).to(self.device) - self.model.load_state_dict(base_model.state_dict(), strict=False) - - # LLM for causality verification - provider = os.getenv("LLM_PROVIDER", "minimax") - model_id = os.getenv("LLM_MODEL", "Qwen") - self.llm_agent = Agent(model=get_model(provider, model_id)) - - def discover_shocks( - self, ticker_list, threshold=2.0, limit_per_stock=5, days=365, pred_len=5 - ): - """1. Find days with significant price movements (Look back 1 year)""" - shocks = [] - end_date = datetime.now().strftime("%Y-%m-%d") - start_date = (datetime.now() - timedelta(days=days)).strftime("%Y-%m-%d") - - for ticker in ticker_list: - df = self.tools.get_stock_price( - ticker, start_date=start_date, end_date=end_date - ) - if df.empty or len(df) < 60: - continue - - # Look for big moves - moves = df[df["change_pct"].abs() > threshold].copy() - if moves.empty: - continue - - count = 0 - for idx, row in moves.iterrows(): - # Ensure we have history before this day AND enough future days for eval - date_idx = df.index.get_loc(idx) - if date_idx < 50 or date_idx + pred_len > len(df): - continue - - shocks.append( - { - "ticker": ticker, - "date": row["date"], - "change": row["change_pct"], - "history": df.iloc[date_idx - 50 : date_idx], - "target": df.iloc[ - date_idx : date_idx + pred_len - ], # Now capturing pred_len days - } - ) - count += 1 - if count >= limit_per_stock: - break - - logger.info( - f"✨ Discovered {len(shocks)} potential price shocks over the last {days} days." - ) - return shocks - - def find_reason_and_verify(self, shock): - """2. Search for reasons and verify causality using LLM""" - ticker_info = self.db.get_stock_by_code(shock["ticker"]) - name = ticker_info["name"] if ticker_info else shock["ticker"] - date_str = shock["date"] - - # Try multiple query variations and engines - queries = [ - f"{name} ({shock['ticker']}) {date_str} 为什么涨跌 原因", - f"{name} {date_str} 异动 原因", - f"{shock['ticker']} {date_str} 新闻", - ] - - search_results = [] - for query in queries: - logger.info(f"🔍 Searching for reason: {query}") - # Try alternate engines - for engine in ["baidu"]: - try: - results = self.searcher.search_list( - query, engine=engine, max_results=3, enrich=False - ) - if results: - search_results = results - break - except Exception as e: - logger.warning(f"Search failed for {query} on {engine}: {e}") - - if search_results: - break - time.sleep(random.uniform(1.0, 2.0)) - - if not search_results: - logger.warning( - f"⚠️ No search results found for {name} on {date_str} after multiple attempts." - ) - return None - - context = "\n".join( - [f"- {r['title']}: {r.get('content', '')[:300]}" for r in search_results] - ) - - prompt = f""" - 任务:判断以下新闻是否解释了该股票在 {date_str} 的 {shock["change"]:.2f}% 价格变动。 - - 股票:{name} - 日期:{date_str} - 变动:{shock["change"]:.2f}% - - 搜索结果: - {context} - - 要求: - 1. 该新闻是否在该日期左右发生? - 2. 该新闻是否能逻辑上解释这种大幅波动(如财报、利好政策、重组、大环境暴跌等)? - 3. 如果是,请总结一段 100 字以内的“核心推动原因”。 - 4. 返回 JSON: {{"is_causal": true/false, "summary": "原因摘要"}} - """ - - try: - res = self.llm_agent.run(prompt) - data = json.loads( - res.content.replace("```json", "").replace("```", "").strip() - ) - if data.get("is_causal"): - logger.success( - f"✅ Verified cause for {name} on {date_str}: {data['summary']}" - ) - return data["summary"] - else: - logger.warning( - f"❌ Verified cause for {name} on {date_str}: {data['summary']}" - ) - return None - except Exception as e: - logger.warning(f"Verification failed: {e}") - return None - - def save_model(self, path=None): - """Save the news_proj weights""" - if path is None: - save_dir = os.path.join(SRC_DIR, "exports/models") - os.makedirs(save_dir, exist_ok=True) - path = os.path.join( - save_dir, f"kronos_news_v1_{datetime.now().strftime('%Y%m%d_%H%M')}.pt" - ) - - # We only really need to save the news_proj part as it's the only one we train - torch.save( - { - "news_proj_state_dict": self.model.news_proj.state_dict(), - "news_dim": self.news_dim, - "d_model": self.model.d_model, - }, - path, - ) - logger.success(f"💾 Model weights saved to {path}") - return path - - def run_synthesis_and_train(self, tickers, pred_len=5): - # 1. Discovery - shocks = self.discover_shocks(tickers, pred_len=pred_len) - print(f"find {len(shocks)} shocks") - - # 2. News Association & Verification - dataset = [] - max_news_items = 200 # Limit to 200 news items per session to avoid search bans - - logger.info( - f"🧬 Starting News Association for {len(shocks)} shocks (Max limit: {max_news_items})" - ) - - for i, shock in enumerate(shocks): - if len(dataset) >= max_news_items: - logger.info("Reached maximum news items limit for this session.") - break - - summary = self.find_reason_and_verify(shock) - if summary: - # 3. Embedding news - emb = self.embedder.encode(summary) - dataset.append( - { - "history": shock["history"], - "target": shock["target"], - "news_emb": emb, - "summary": summary, - } - ) - - # Add delay after search with randomness to avoid being blocked - if i < len(shocks) - 1: - delay = random.uniform(2.0, 4.0) - time.sleep(delay) - - if not dataset: - logger.error( - "❌ No verified news-price pairs found. Adjust threshold or check if news is available in that period." - ) - return - - # 4. Train/Val Split - random.seed(42) - random.shuffle(dataset) - - if len(dataset) < 2: - train_set = dataset - val_set = [] - logger.warning( - f"⚠️ Only {len(dataset)} sample(s) found. Training on all, skipping validation." - ) - else: - split_idx = max(1, int(len(dataset) * 0.8)) - if split_idx >= len(dataset): - split_idx = len(dataset) - 1 - - train_set = dataset[:split_idx] - val_set = dataset[split_idx:] - logger.info( - f"🏗️ Dataset Split: {len(train_set)} samples for training, {len(val_set)} for validation." - ) - - if not train_set: - logger.error("❌ No samples for training.") - return - - # 5. Training (Few-shot) - optimizer = torch.optim.Adam(self.model.news_proj.parameters(), lr=1e-3) - criterion = nn.CrossEntropyLoss() - self.model.train() - - loss_history = [] - logger.info(f"🚀 Training for 30 epochs...") - for epoch in range(30): - total_loss = 0 - for item in train_set: - optimizer.zero_grad() - - # Prep Data - hist_df = item["history"] - # For training, we still focus on the immediate next point (teacher forcing) - target_df = item["target"].iloc[:1] - - hist_raw = hist_df[ - ["open", "high", "low", "close", "volume"] - ].values.astype(np.float32) - hist_raw = np.column_stack([hist_raw, hist_raw[:, 3] * hist_raw[:, 4]]) - - mean, std = hist_raw.mean(axis=0), hist_raw.std(axis=0) + 1e-5 - hist_norm = ( - torch.from_numpy((hist_raw - mean) / std) - .unsqueeze(0) - .to(self.device) - ) - - target_raw = target_df[ - ["open", "high", "low", "close", "volume"] - ].values.astype(np.float32) - target_raw = np.column_stack( - [target_raw, target_raw[:, 3] * target_raw[:, 4]] - ) - target_norm = ( - torch.from_numpy((target_raw - mean) / std) - .unsqueeze(0) - .to(self.device) - ) - - with torch.no_grad(): - z_indices = self.tokenizer.encode(hist_norm, half=True) - t_indices = self.tokenizer.encode(target_norm, half=True) - s1_ids, s2_ids = z_indices[0], z_indices[1] - t_s1, t_s2 = t_indices[0], t_indices[1] - - news_t = torch.from_numpy(item["news_emb"]).unsqueeze(0).to(self.device) - s1_logits, s2_logits = self.model( - s1_ids, - s2_ids, - news_emb=news_t, - use_teacher_forcing=True, - s1_targets=t_s1, - ) - - loss = ( - criterion(s1_logits[:, -1, :], t_s1[:, 0]) - + criterion(s2_logits[:, -1, :], t_s2[:, 0]) - ) / 2 - loss.backward() - optimizer.step() - total_loss += loss.item() - - avg_epoch_loss = total_loss / max(1, len(train_set)) - loss_history.append(avg_epoch_loss) - - if (epoch + 1) % 10 == 0: - logger.info(f"Epoch {epoch + 1} Loss: {avg_epoch_loss:.4f}") - - # 5.1 Visualize Loss Curve - loss_chart = VisualizerTools.generate_loss_chart(loss_history) - VisualizerTools.render_chart_to_file( - loss_chart, - os.path.join(SRC_DIR, "exports/training_results/loss_curve.html"), - ) - - # 5.2 Save final model - self.save_model() - - # 6. Final Evaluation on Validation Set - if not val_set: - logger.warning("⚠️ Validation set is empty. Skipping statistical analysis.") - return - - logger.info( - f"🧪 Final Evaluation: Base vs News-Integrated ({pred_len}-day Window)" - ) - self.model.eval() - predictor = KronosPredictor(self.model, self.tokenizer, device=self.device) - - base_maes = [] - news_maes = [] - - print("\n" + "=" * 90) - print( - f"{'Date':<12} | {'Ticker':<8} | {'Base MAE':<15} | {'News MAE':<15} | {'Improvement'}" - ) - print("-" * 90) - - for item in val_set: - h = item["history"] - t = item["target"] - actuals = t["close"].values[:pred_len] - - x_ts = pd.to_datetime(h["date"]) - # Future timestamps: handle business days if possible, or just simple offset - future_dates = pd.date_range( - start=x_ts.iloc[-1] + timedelta(days=1), periods=pred_len, freq="B" - ) - y_ts = pd.Series(future_dates) - - # A. Base Prediction - p_base = predictor.predict( - h, x_ts, y_ts, pred_len=pred_len, news_emb=None, verbose=False - ) - b_preds = p_base["close"].values[: len(actuals)] - - # B. News-Aware Prediction - p_news = predictor.predict( - h, - x_ts, - y_ts, - pred_len=pred_len, - news_emb=item["news_emb"], - verbose=False, - ) - n_preds = p_news["close"].values[: len(actuals)] - - # Calculate MAE over the window - b_mae = np.mean(np.abs(b_preds - actuals)) - n_mae = np.mean(np.abs(n_preds - actuals)) - - base_maes.append(b_mae) - news_maes.append(n_mae) - - improvement = (b_mae - n_mae) / (b_mae + 1e-6) * 100 - - date_str = str(t["date"].values[0])[:10] - ticker = h.iloc[-1]["ticker"] if "ticker" in h.columns else "Stock" - print( - f"{date_str:<12} | {ticker:<8} | {b_mae:<15.4f} | {n_mae:<15.4f} | {improvement:>+7.1f}%" - ) - - # C. Generate Visualization for this case - try: - # Helper to convert DF to KLinePoints - def to_kp_list(preds_df): - points = [] - for idx, row in preds_df.iterrows(): - points.append( - KLinePoint( - date=str(idx)[:10], - open=row["open"], - high=row["high"], - low=row["low"], - close=row["close"], - volume=row["volume"] if "volume" in row else 0, - ) - ) - return points - - forecast_obj = ForecastResult( - ticker=ticker, - base_forecast=to_kp_list(p_base), - adjusted_forecast=to_kp_list(p_news), - rationale=item["summary"], - ) - - # Ground truth for visualizer expects a DataFrame with 'date' and 'close' - gt_df = t[["date", "open", "high", "low", "close", "volume"]] - - chart = VisualizerTools.generate_stock_chart( - df=h, - ticker=ticker, - title=f"Training Eval: {ticker} ({date_str}) Improvement: {improvement:.1f}%", - forecast=forecast_obj, - ground_truth=gt_df, - ) - - safe_date = date_str.replace("-", "") - filename = f"eval_{ticker}_{safe_date}.html" - VisualizerTools.render_chart_to_file( - chart, os.path.join(SRC_DIR, f"exports/training_results/{filename}") - ) - except Exception as e: - logger.error(f"Failed to generate eval chart for {ticker}: {e}") - - # Summary Statistics - avg_base_err = sum(base_maes) / max(1, len(base_maes)) - avg_news_err = sum(news_maes) / max(1, len(news_maes)) - overall_imp = (avg_base_err - avg_news_err) / (avg_base_err + 1e-6) * 100 - - print("-" * 90) - print( - f"{'AVERAGE':<12} | {'-':<8} | {avg_base_err:<15.4f} | {avg_news_err:<15.4f} | {overall_imp:>+7.1f}%" - ) - print("=" * 90 + "\n") - - logger.success( - f"🏁 Statistical Analysis Complete. Avg Error Reduction ({pred_len}-day): {overall_imp:.2f}%" - ) - logger.info( - f"📊 Visualization results saved to: {os.path.join(SRC_DIR, 'exports/training_results/')}" - ) - - -if __name__ == "__main__": - trainer = AutoSynthesisTrainer() - - logger.info("📂 Fetching all stock codes from database...") - res = trainer.db.execute_query("SELECT code FROM stock_list") - all_tickers = [row["code"] for row in res] - - if not all_tickers: - logger.warning("⚠️ No tickers found in stock_list table. Trying to sync...") - trainer.tools._check_and_update_stock_list(force=True) - res = trainer.db.execute_query("SELECT code FROM stock_list") - all_tickers = [row["code"] for row in res] - - logger.info(f"🚀 Starting training on potential stocks (1-year scan)...") - # 为了演示,我们扫描前 100 个股票,寻找最近一年的冲击点 - trainer.run_synthesis_and_train(all_tickers[:100], pred_len=1) diff --git a/skills/alphaear-reporter/scripts/utils/search_tools.py b/skills/alphaear-reporter/scripts/utils/search_tools.py deleted file mode 100644 index 50b08f3..0000000 --- a/skills/alphaear-reporter/scripts/utils/search_tools.py +++ /dev/null @@ -1,611 +0,0 @@ -import os -import hashlib -import json -import re -import requests -import time -import threading -from typing import List, Dict, Optional, Any -from agno.tools.duckduckgo import DuckDuckGoTools -from agno.tools.baidusearch import BaiduSearchTools -from agno.agent import Agent -from loguru import logger -from datetime import datetime -from .database_manager import DatabaseManager -from .content_extractor import ContentExtractor -from .llm.factory import get_model -from .hybrid_search import LocalNewsSearch - -# 默认搜索缓存 TTL(秒),可通过环境变量覆盖 -DEFAULT_SEARCH_TTL = int(os.getenv("SEARCH_CACHE_TTL", "3600")) # 默认 1 小时 - - -class JinaSearchEngine: - """Jina Search API 封装 - 使用 s.jina.ai 进行网络搜索""" - - JINA_SEARCH_URL = "https://s.jina.ai/" - - # 速率限制配置 - _rate_limit_no_key = 10 # 无 key 时每分钟最大请求数 - _rate_window = 60.0 - _min_interval = 2.0 - _request_times = [] - _last_request_time = 0.0 - _lock = threading.Lock() - - def __init__(self): - self.api_key = os.getenv("JINA_API_KEY", "").strip() - self.has_api_key = bool(self.api_key) - if self.has_api_key: - logger.info("✅ Jina Search API key configured") - - @classmethod - def _wait_for_rate_limit(cls, has_api_key: bool) -> None: - """等待以满足速率限制""" - if has_api_key: - time.sleep(0.3) - return - - with cls._lock: - current_time = time.time() - cls._request_times = [t for t in cls._request_times if current_time - t < cls._rate_window] - - if len(cls._request_times) >= cls._rate_limit_no_key: - oldest = cls._request_times[0] - wait_time = cls._rate_window - (current_time - oldest) + 1.0 - if wait_time > 0: - logger.warning(f"⏳ Jina Search rate limit, waiting {wait_time:.1f}s...") - time.sleep(wait_time) - current_time = time.time() - cls._request_times = [t for t in cls._request_times if current_time - t < cls._rate_window] - - time_since_last = current_time - cls._last_request_time - if time_since_last < cls._min_interval: - time.sleep(cls._min_interval - time_since_last) - - cls._request_times.append(time.time()) - cls._last_request_time = time.time() - - def search(self, query: str, max_results: int = 5) -> List[Dict]: - """ - 使用 Jina Search API 执行搜索 - - Args: - query: 搜索关键词 - max_results: 返回结果数量 - - Returns: - 搜索结果列表,每个结果包含 title, url, content - """ - if not query: - return [] - - logger.info(f"🔍 Jina Search: {query}") - - # 等待速率限制 - self._wait_for_rate_limit(self.has_api_key) - - headers = { - "Accept": "application/json", - "X-Retain-Images": "none", - } - - if self.has_api_key: - headers["Authorization"] = f"Bearer {self.api_key}" - - try: - # Jina Search API: https://s.jina.ai/{query} - import urllib.parse - encoded_query = urllib.parse.quote(query) - url = f"{self.JINA_SEARCH_URL}{encoded_query}" - - response = requests.get(url, headers=headers, timeout=30) - - if response.status_code == 429: - logger.warning("⚠️ Jina Search rate limited (429), waiting 30s...") - time.sleep(30) - return self.search(query, max_results) - - if response.status_code != 200: - logger.warning(f"Jina Search failed (Status {response.status_code})") - return [] - - # 解析响应 - try: - data = response.json() - except json.JSONDecodeError: - # 如果返回纯文本,尝试解析 - data = {"data": [{"title": "Search Result", "url": "", "content": response.text}]} - - results = [] - - # Jina 返回格式可能是 {"data": [...]} 或直接是列表 - items = data.get("data", []) if isinstance(data, dict) else data - if not isinstance(items, list): - items = [items] if items else [] - - for i, item in enumerate(items[:max_results]): - if isinstance(item, dict): - results.append({ - "title": item.get("title", f"Result {i+1}"), - "url": item.get("url", ""), - "href": item.get("url", ""), # 兼容性 - "content": item.get("content", item.get("description", "")), - "body": item.get("content", item.get("description", "")), # 兼容性 - }) - elif isinstance(item, str): - results.append({ - "title": f"Result {i+1}", - "url": "", - "content": item - }) - - logger.info(f"✅ Jina Search returned {len(results)} results") - return results - - except requests.exceptions.Timeout: - logger.error("Jina Search timeout") - return [] - except requests.exceptions.RequestException as e: - logger.error(f"Jina Search request error: {e}") - return [] - except Exception as e: - logger.error(f"Jina Search unexpected error: {e}") - return [] - -class SearchTools: - """扩展性搜索工具库 - 支持多引擎聚合与内容缓存""" - - def __init__(self, db: DatabaseManager): - self.db = db - - # 检查 Jina API Key 是否配置 - jina_api_key = os.getenv("JINA_API_KEY", "").strip() - self._jina_enabled = bool(jina_api_key) - - self._engines = { - "ddg": DuckDuckGoTools(), - "baidu": BaiduSearchTools(), - "local": LocalNewsSearch(db) - } - - # 如果配置了 Jina API Key,添加 Jina 引擎 - if self._jina_enabled: - self._engines["jina"] = JinaSearchEngine() - logger.info("🚀 Jina Search engine enabled (JINA_API_KEY configured)") - - # 确定默认搜索引擎 - self._default_engine = "jina" if self._jina_enabled else "ddg" - - def _generate_hash(self, query: str, engine: str, max_results: int) -> str: - return hashlib.md5(f"{engine}:{query}:{max_results}".encode()).hexdigest() - - def search(self, query: str, engine: str = None, max_results: int = 5, ttl: Optional[int] = None) -> str: - """ - 使用指定搜索引擎执行网络搜索,结果会被缓存以提高效率。 - - Args: - query: 搜索关键词,如 "英伟达财报" 或 "光伏行业政策"。 - engine: 搜索引擎选择。可选值: - "jina" (Jina Search,需配置 JINA_API_KEY,LLM友好输出), - "ddg" (DuckDuckGo,推荐英文/国际搜索), - "baidu" (百度,推荐中文/国内搜索), - "local" (本地历史新闻搜索,基于向量+BM25)。 - 默认: 若配置了 JINA_API_KEY 则使用 "jina",否则 "ddg"。 - max_results: 期望返回的结果数量,默认 5 条。 - ttl: 缓存有效期(秒)。如果缓存超过此时间会重新搜索。 - 默认使用环境变量 SEARCH_CACHE_TTL 或 3600 秒。 - 设为 0 可强制刷新。 - - Returns: - 搜索结果的文本描述,包含标题、摘要和链接。 - """ - # 使用默认引擎(如果配置了 Jina 则优先使用 Jina) - if engine is None: - engine = self._default_engine - - if engine not in self._engines: - return f"Error: Unsupported engine '{engine}'. Available: {list(self._engines.keys())}" - - query_hash = self._generate_hash(query, engine, max_results) - effective_ttl = ttl if ttl is not None else DEFAULT_SEARCH_TTL - - # 1. 尝试从缓存读取 (local 引擎不缓存,因为它本身就是查库) - if engine != "local": - cache = self.db.get_search_cache(query_hash, ttl_seconds=effective_ttl if effective_ttl > 0 else None) - if cache and effective_ttl != 0: - logger.info(f"ℹ️ Found search results in cache for: {query} ({engine})") - return cache['results'] - - # 2. 执行真实搜索 - logger.info(f"📡 Searching {engine} for: {query}") - try: - tool = self._engines[engine] - if engine == "jina": - # Jina Search 返回 List[Dict] - jina_results = tool.search(query, max_results=max_results) - results = [] - for r in jina_results: - results.append({ - "title": r.get("title", ""), - "href": r.get("url", ""), - "body": r.get("content", "") - }) - elif engine == "ddg": - results = tool.duckduckgo_search(query, max_results=max_results) - elif engine == "baidu": - results = tool.baidu_search(query, max_results=max_results) - elif engine == "local": - # LocalNewsSearch 返回的是 List[Dict] - local_results = tool.search(query, top_n=max_results) - results = [] - for r in local_results: - results.append({ - "title": r.get("title"), - "href": r.get("url", "local"), - "body": r.get("content", "") - }) - else: - results = "Search not implemented for this engine." - - results_str = str(results) - if engine != "local": - self.db.save_search_cache(query_hash, query, engine, results_str) - return results_str - - except Exception as e: - # 搜索失败时的降级策略 - if engine == "jina": - logger.warning(f"⚠️ Jina search failed, falling back to ddg: {query} ({e})") - try: - return self.search(query, engine="ddg", max_results=max_results, ttl=ttl) - except Exception as e2: - logger.error(f"❌ DDG fallback also failed for {query}: {e2}") - elif engine == "ddg": - logger.warning(f"⚠️ DDG search failed, falling back to baidu: {query} ({e})") - try: - return self.search(query, engine="baidu", max_results=max_results, ttl=ttl) - except Exception as e2: - logger.error(f"❌ Baidu fallback also failed for {query}: {e2}") - - logger.error(f"❌ Search failed for {query}: {e}") - return f"Error occurred during search: {str(e)}" - - def search_list(self, query: str, engine: str = None, max_results: int = 5, ttl: Optional[int] = None, enrich: bool = True) -> List[Dict]: - """ - 执行搜索并返回结构化列表 (List[Dict])。 - Dict 包含: title, href (or url), body (or snippet) - - Args: - engine: 搜索引擎,默认使用配置的默认引擎(Jina 优先) - enrich: 是否抓取正文内容 (默认 True) - """ - # 使用默认引擎 - if engine is None: - engine = self._default_engine - - if engine not in self._engines: - logger.error(f"Unsupported engine {engine}") - return [] - - # 不同的 hash 以区分是否 enrichment - enrich_suffix = ":enriched" if enrich else "" - query_hash = self._generate_hash(query, engine + enrich_suffix, max_results) - effective_ttl = ttl if ttl is not None else DEFAULT_SEARCH_TTL - - # 1. 尝试从缓存读取 - cache = self.db.get_search_cache(query_hash, ttl_seconds=effective_ttl if effective_ttl > 0 else None) - if cache and effective_ttl != 0: - try: - cached_data = json.loads(cache['results']) - if isinstance(cached_data, list): - logger.info(f"ℹ️ Found structured search cache for: {query}") - return cached_data - except: - pass - - # 1.5 Smart Cache (Fuzzy + LLM) - if effective_ttl != 0: - try: - # 1. Similar cached queries - similar_queries = self.db.find_similar_queries(query, limit=3) - # Filter by TTL - valid_candidates = [] - for q in similar_queries: - if q['query'] == query: continue - q_time = datetime.fromisoformat(q['timestamp']) - if effective_ttl and (datetime.now() - q_time).total_seconds() > effective_ttl: - continue - q['type'] = 'cached_search' - valid_candidates.append(q) - - # 2. Relevant local news (as search results) - local_news = self.db.search_local_news(query, limit=3) - if local_news: - # Group local news as a single "candidate" source? Or individual? - # Better to treat "Local News Database" as one candidate source that contains X items. - # Or just add them to candidates list? - # Let's package strictly relevant news as a "local_news_bundle" - valid_candidates.append({ - 'type': 'local_news', - 'query': 'Local Database News', - 'items': local_news, - 'timestamp': datetime.now().isoformat() - }) - - if valid_candidates: - logger.info(f"🤔 Found {len(valid_candidates)} smart cache candidates (Queries/News). Asking LLM...") - evaluation = self._evaluate_cache_relevance(query, valid_candidates) - - if evaluation and evaluation.get('reuse', False): - idx = evaluation.get('index', -1) - if 0 <= idx < len(valid_candidates): - chosen = valid_candidates[idx] - logger.info(f"🤖 LLM suggested reusing: '{chosen.get('query')}' ({chosen['type']})") - - if chosen['type'] == 'cached_search': - # Load the chosen cache - cache = self.db.get_search_cache(chosen['query_hash']) - if cache: - try: - cached_data = json.loads(cache['results']) - if isinstance(cached_data, list): - return cached_data - except: - pass - elif chosen['type'] == 'local_news': - # Convert local news items to search result format - news_results = [] - for i, news in enumerate(chosen['items'], 1): - news_results.append({ - "id": news.get('id'), - "rank": i, - "title": news.get('title'), - "url": news.get('url'), - "content": news.get('content'), - "original_snippet": news.get('content')[:200] if news.get('content') else '', - "source": f"Local News ({news.get('source')})", - "publish_time": news.get('publish_time'), - "crawl_time": news.get('crawl_time'), - "sentiment_score": news.get('sentiment_score', 0), - "meta_data": {"origin": "local_db"} - }) - return news_results - - except Exception as e: - logger.warning(f"Smart cache check failed: {e}") - - # 2. 执行搜索 - logger.info(f"📡 Searching {engine} (structured) for: {query}") - try: - tool = self._engines[engine] - results = [] - if engine == "jina": - # Jina Search 直接返回结构化数据 - jina_results = tool.search(query, max_results=max_results) - for r in jina_results: - results.append({ - "title": r.get("title", ""), - "url": r.get("url", ""), - "href": r.get("url", ""), - "body": r.get("content", ""), - "content": r.get("content", ""), - "source": "Jina Search" - }) - elif engine == "ddg": - results = tool.duckduckgo_search(query, max_results=max_results) - elif engine == "baidu": - results = tool.baidu_search(query, max_results=max_results) - elif engine == "local": - # LocalNewsSearch 返回的是 List[Dict] - local_results = tool.search(query, top_n=max_results) - results = [] - for r in local_results: - results.append({ - "title": r.get("title"), - "url": r.get("url", "local"), - "body": r.get("content", "")[:500], - "source": f"Local ({r.get('source', 'db')})", - "publish_time": r.get("publish_time") - }) - - # 处理字符串类型的 JSON 返回 (Baidu 常返 JSON 字符串) - if isinstance(results, str) and engine not in ["local", "jina"]: - try: - results = json.loads(results) - except: - pass - - # 转为统一格式 - normalized_results = [] - if isinstance(results, list): - - for i, r in enumerate(results, 1): - title = r.get('title', '') - url = r.get('href') or r.get('url') or r.get('link', '') - content = r.get('body') or r.get('snippet') or r.get('abstract', '') - - if title and url: - normalized_results.append({ - "id": self._generate_hash(url + query, "search_item", i), - "rank": i, - "title": title, - "url": url, - "content": content, - "original_snippet": content, # 保留摘要 - "source": f"Search ({engine})", - "publish_time": datetime.now().isoformat(), # 暂用当前时间 - "crawl_time": datetime.now().isoformat(), - "meta_data": {"query": query, "engine": engine} - }) - - # Fallback if still string and failed to parse - elif isinstance(results, str) and results: - normalized_results.append({"title": query, "url": "", "content": results, "source": engine}) - - # 3. 抓取正文 & 计算情绪 (Enrichment) - # 注意:如果使用 Jina Search,内容已经是 LLM 友好格式,可选择跳过 enrichment - skip_content_enrichment = (engine == "jina") - - if enrich and normalized_results: - logger.info(f"🕸️ Enriching {len(normalized_results)} search results with Jina & Sentiment...") - extractor = ContentExtractor() - - # Lazy load sentiment tool - if not hasattr(self, 'sentiment_tool') or self.sentiment_tool is None: - from ..sentiment_tools import SentimentTools - self.sentiment_tool = SentimentTools(self.db) - - for item in normalized_results: - if item.get("url"): - try: - # 如果是 Jina Search,内容已经足够好,跳过额外抓取 - if skip_content_enrichment and item.get("content") and len(item.get("content", "")) > 100: - full_content = item["content"] - else: - # Use Jina Reader to get full content - full_content = extractor.extract_with_jina(item["url"], timeout=60) - - if full_content and len(full_content) > 100: - item["content"] = full_content - - # Calculate sentiment - # Use title + snippet of content for efficiency - text_to_analyze = f"{item['title']} {full_content[:500]}" - sent_result = self.sentiment_tool.analyze_sentiment(text_to_analyze) # Using self.sentiment_tool - score = sent_result.get('score', 0.0) - item["sentiment_score"] = float(score) - - logger.info(f" ✅ Enriched: {item['title'][:20]}... (Sentiment: {score:.2f})") - else: - # Fallback: Use snippet for sentiment - logger.info(f" ⚠️ Content short/failed for {item['url']}, using snippet for sentiment.") - text_to_analyze = f"{item['title']} {item['content']}" # content is snippet here - sent_result = self.sentiment_tool.analyze_sentiment(text_to_analyze) - score = sent_result.get('score', 0.0) - item["sentiment_score"] = float(score) - - except Exception as e: - # Fallback: Use snippet for sentiment on error - logger.warning(f"Failed to enrich {item['url']}: {e}. Using snippet.") - text_to_analyze = f"{item['title']} {item['content']}" - sent_result = self.sentiment_tool.analyze_sentiment(text_to_analyze) - score = sent_result.get('score', 0.0) - item["sentiment_score"] = float(score) - - # 缓存结果 list - if normalized_results: - # Pass list directly, DB manager will handle JSON dump for main cache and populate search_details - # Only cache if NOT from local news reuse (though this logic path is for fresh search) - self.db.save_search_cache(query_hash, query, engine, normalized_results) - - return normalized_results - - except Exception as e: - # 搜索失败时的降级策略 - if engine == "jina": - logger.warning(f"⚠️ Jina search_list failed, falling back to ddg: {query} ({e})") - try: - return self.search_list(query, engine="ddg", max_results=max_results, ttl=ttl, enrich=enrich) - except Exception as e2: - logger.error(f"❌ DDG fallback (search_list) also failed for {query}: {e2}") - elif engine == "ddg": - logger.warning(f"⚠️ DDG search_list failed, falling back to baidu: {query} ({e})") - try: - return self.search_list(query, engine="baidu", max_results=max_results, ttl=ttl, enrich=enrich) - except Exception as e2: - logger.error(f"❌ Baidu fallback (search_list) also failed for {query}: {e2}") - - logger.error(f"❌ Structured search failed for {query}: {e}") - return [] - - def _evaluate_cache_relevance(self, current_query: str, candidates: List[Dict]) -> Dict: - """ - 使用 LLM 评估缓存候选是否足以回答当前问题。 - """ - try: - # Prepare candidates text - candidates_desc = [] - for i, c in enumerate(candidates): - if c['type'] == 'cached_search': - # Preview cached results if available? - # Maybe just use the query string as a proxy for what's in there. - # Or peek at 'results' snippet. - preview = "" - try: - # Attempt to peek first result title from JSON string - # Note: c.get('results') might be a stringified JSON list - res_list = json.loads(c.get('results', '[]')) - if res_list and isinstance(res_list, list) and len(res_list) > 0: - first_item = res_list[0] - if isinstance(first_item, dict) and 'title' in first_item: - preview = f" (Contains: {first_item.get('title', '')[:50]}...)" - except: - pass - candidates_desc.append(f"[{i}] Old Search Query: '{c['query']}' {preview} (Time: {c['timestamp']})") - elif c['type'] == 'local_news': - # List titles of local news - titles = [item['title'] for item in c['items'][:3]] - candidates_desc.append(f"[{i}] Local Database News: {', '.join(titles)}... (Time: {c['timestamp']})") - - prompt = f""" - Task: Decide if existing information is sufficient for the new search query. - - New Query: "{current_query}" - - Available Information Candidates: - {chr(10).join(candidates_desc)} - - Instructions: - 1. Analyze if any candidate provides ENOUGH up-to-date info for the "New Query". - 2. If yes, choose the best one. - 3. If the query implies needing LATEST real-time info and candidates are old, choose none. - 4. Return strictly JSON: {{"reuse": true/false, "index": , "reason": "short explanation"}} - """ - # 初始化模型 - provider = os.getenv("LLM_PROVIDER", "minimax") - model_id = os.getenv("LLM_MODEL", "Qwen") - host = os.getenv("LLM_HOST") - if host: - model = get_model(provider, model_id, host=host) - else: - model = get_model(provider, model_id) - - agent = Agent(model=model, markdown=True) - - response = agent.run(prompt) - content = response.content - - # Parse JSON - json_match = re.search(r'```json\s*(.*?)\s*```', content, re.DOTALL) - if json_match: - return json.loads(json_match.group(1)) - elif '{' in content: - # Fallback for cases where LLM doesn't wrap in ```json - return json.loads(content[content.find('{'):content.rfind('}')+1]) - return {"reuse": False} - - except Exception as e: - logger.warning(f"LLM evaluation failed: {e}") - return {"reuse": False} - - def aggregate_search(self, query: str, engines: Optional[List[str]] = None, max_results: int = 5) -> str: - """ - 使用多个搜索引擎同时搜索并聚合结果,获得更全面的信息覆盖。 - - Args: - query: 搜索关键词。 - engines: 要使用的搜索引擎列表。可选值: ["ddg", "baidu"]。 - 默认同时使用 ddg 和 baidu。 - max_results: 每个引擎期望返回的结果数量。 - - Returns: - 聚合后的搜索结果,按引擎分组显示。 - """ - engines = engines or ["ddg", "baidu"] - aggregated_results = [] - for engine in engines: - res = self.search(query, engine=engine, max_results=max_results) - aggregated_results.append(f"--- Results from {engine.upper()} ---\n{res}") - - return "\n\n".join(aggregated_results) diff --git a/skills/alphaear-reporter/scripts/utils/sentiment_tools.py b/skills/alphaear-reporter/scripts/utils/sentiment_tools.py deleted file mode 100644 index f4278b5..0000000 --- a/skills/alphaear-reporter/scripts/utils/sentiment_tools.py +++ /dev/null @@ -1,287 +0,0 @@ -import os -from typing import Dict, List, Union, Optional -import json -from loguru import logger -from agno.agent import Agent -from .llm.factory import get_model -from .database_manager import DatabaseManager - -# 从环境变量读取默认情绪分析模式 -DEFAULT_SENTIMENT_MODE = os.getenv("SENTIMENT_MODE", "auto") # auto, bert, llm - - -class SentimentTools: - """ - 情绪分析工具 - 支持 LLM 和 BERT 两种模式 - - 模式说明: - - "auto": 自动选择,优先使用 BERT(速度快),不可用时回退到 LLM - - "bert": 强制使用 BERT 模型(需要 transformers 库) - - "llm": 强制使用 LLM(更准确但较慢) - - 可通过环境变量 SENTIMENT_MODE 设置默认模式。 - """ - - def __init__( - self, - db: DatabaseManager, - mode: Optional[str] = None, - model_provider: str = "openai", - model_id: str = "gpt-4o", - ): - """ - 初始化情绪分析工具。 - - Args: - db: 数据库管理器实例 - mode: 分析模式,可选 "auto", "bert", "llm"。None 则使用环境变量默认值。 - model_provider: LLM 提供商,如 "openai", "ust", "deepseek" - model_id: 模型标识符 - """ - self.db = db - self.mode = mode or DEFAULT_SENTIMENT_MODE - self.llm_model = None - self.bert_pipeline = None - - # Initialize LLM - try: - provider = "minimax" if os.getenv("MINIMAX_API_KEY") else model_provider - m_id = ( - os.getenv("LLM_MODEL", "MiniMax-Text-01") - if provider == "minimax" - else model_id - ) - self.llm_model = get_model(provider, m_id) - except Exception as e: - logger.warning(f"LLM initialization skipped: {e}") - - # Initialize BERT if needed - if self.mode in ["bert", "auto"]: - try: - from transformers import ( - pipeline, - AutoTokenizer, - AutoModelForSequenceClassification, - ) - from transformers.utils import logging as transformers_logging - - transformers_logging.set_verbosity_error() # 减少冗余日志 - - bert_model = os.getenv( - "BERT_SENTIMENT_MODEL", - "uer/roberta-base-finetuned-chinanews-chinese", - ) - - # 优先使用本地缓存 - try: - tokenizer = AutoTokenizer.from_pretrained( - bert_model, local_files_only=True - ) - model = AutoModelForSequenceClassification.from_pretrained( - bert_model, local_files_only=True - ) - - self.bert_pipeline = pipeline( - "sentiment-analysis", - model=model, - tokenizer=tokenizer, - device=-1, - ) - logger.info( - f"✅ BERT pipeline loaded from local cache: {bert_model}" - ) - except (OSError, ValueError, ImportError): - # 本地没有,则从网络下载 - logger.info(f"📡 Downloading BERT model: {bert_model}...") - tokenizer = AutoTokenizer.from_pretrained(bert_model) - model = AutoModelForSequenceClassification.from_pretrained( - bert_model - ) - - self.bert_pipeline = pipeline( - "sentiment-analysis", - model=model, - tokenizer=tokenizer, - device=-1, - ) - logger.info( - f"✅ BERT Sentiment pipeline ({bert_model}) initialized." - ) - except ImportError: - logger.warning( - "Transformers library not installed. BERT sentiment analysis disabled." - ) - except Exception as e: - if self.mode == "bert": - logger.error(f"BERT mode requested but failed: {e}") - else: - logger.warning(f"BERT unavailable, using LLM only. Error: {e}") - self.bert_pipeline = None - - def analyze_sentiment(self, text: str) -> Dict[str, Union[float, str]]: - """ - 分析文本的情绪极性。根据初始化时的 mode 自动选择分析方法。 - - Args: - text: 需要分析的文本内容,如新闻标题或摘要。 - - Returns: - 包含以下字段的字典: - - score: 情绪分值,范围 -1.0(极度负面)到 1.0(极度正面),0.0 为中性 - - label: 情绪标签,"positive"/"negative"/"neutral" - - reason: 分析理由(仅 LLM 模式提供详细理由) - """ - if self.mode == "bert" and self.bert_pipeline: - results = self.analyze_sentiment_bert([text]) - return results[0] if results else {"score": 0.0, "label": "error"} - elif self.mode == "llm" or (self.mode == "auto" and not self.bert_pipeline): - return self.analyze_sentiment_llm(text) - else: - # auto mode with BERT available - results = self.analyze_sentiment_bert([text]) - return results[0] if results else {"score": 0.0, "label": "error"} - - def analyze_sentiment_llm(self, text: str) -> Dict[str, Union[float, str]]: - """ - 使用 LLM 进行深度情绪分析,可获得详细的分析理由。 - - Args: - text: 需要分析的文本,最多处理前 1000 字符。 - - Returns: - 包含 score, label, reason 的字典。 - """ - if not self.llm_model: - return {"score": 0.0, "label": "neutral", "error": "LLM not initialized"} - - analyzer = Agent(model=self.llm_model, markdown=True) - prompt = f"""请分析以下金融/新闻文本的情绪极性。 - 返回严格的 JSON 格式: - {{"score": , "label": "", "reason": "<简短理由>"}} - - 文本: {text[:1000]}""" - - try: - response = analyzer.run(prompt) - content = response.content - if "```json" in content: - content = content.split("```json")[1].split("```")[0].strip() - elif "```" in content: - content = content.split("```")[1].split("```")[0].strip() - return json.loads(content) - except Exception as e: - logger.error(f"LLM sentiment failed: {e}") - return {"score": 0.0, "label": "error", "reason": str(e)} - - def analyze_sentiment_bert(self, texts: List[str]) -> List[Dict]: - """ - 使用 BERT 进行批量高速情绪分析。 - - Args: - texts: 需要分析的文本列表。 - - Returns: - 与输入列表等长的分析结果列表。 - """ - if not self.bert_pipeline: - return [ - {"score": 0.0, "label": "error", "reason": "BERT not available"} - ] * len(texts) - - try: - results = self.bert_pipeline(texts, truncation=True, max_length=512) - processed = [] - for r in results: - label = r["label"].lower() - score = r["score"] - - # 标准化不同模型的标签格式 - if "negative" in label or "neg" in label: - score = -score - elif "neutral" in label or "neu" in label: - score = 0.0 - - processed.append( - { - "score": float(round(score, 3)), - "label": "positive" - if score > 0.1 - else ("negative" if score < -0.1 else "neutral"), - "reason": "BERT automated analysis", - } - ) - return processed - except Exception as e: - logger.error(f"BERT analysis failed: {e}") - return [{"score": 0.0, "label": "error", "reason": str(e)}] * len(texts) - - def batch_update_news_sentiment( - self, - source: Optional[str] = None, - limit: int = 50, - use_bert: Optional[bool] = None, - ): - """ - 批量更新数据库中新闻的情绪分数。 - - Args: - source: 筛选特定新闻源,如 "wallstreetcn"。None 则处理所有来源。 - limit: 最多处理的新闻数量。 - use_bert: 是否使用 BERT。None 则根据初始化模式自动决定。 - - Returns: - 成功更新的新闻数量。 - """ - news_items = self.db.get_daily_news(source=source, limit=limit) - to_analyze = [item for item in news_items if not item.get("sentiment_score")] - - if not to_analyze: - return 0 - - # 决定使用哪种方法 - should_use_bert = ( - use_bert - if use_bert is not None - else (self.bert_pipeline is not None and self.mode != "llm") - ) - - updated_count = 0 - cursor = self.db.conn.cursor() - - if should_use_bert and self.bert_pipeline: - logger.info( - f"🚀 Using BERT for batch analysis of {len(to_analyze)} items..." - ) - titles = [item["title"] for item in to_analyze] - results = self.analyze_sentiment_bert(titles) - - for item, analysis in zip(to_analyze, results): - cursor.execute( - """ - UPDATE daily_news - SET sentiment_score = ?, meta_data = json_set(COALESCE(meta_data, '{}'), '$.sentiment_reason', ?) - WHERE id = ? - """, - (analysis["score"], analysis["reason"], item["id"]), - ) - updated_count += 1 - else: - logger.info(f"🚶 Using LLM for analysis of {len(to_analyze)} items...") - for item in to_analyze: - analysis = self.analyze_sentiment_llm(item["title"]) - cursor.execute( - """ - UPDATE daily_news - SET sentiment_score = ?, meta_data = json_set(COALESCE(meta_data, '{}'), '$.sentiment_reason', ?) - WHERE id = ? - """, - ( - analysis.get("score", 0.0), - analysis.get("reason", ""), - item["id"], - ), - ) - updated_count += 1 - - self.db.conn.commit() - return updated_count diff --git a/skills/alphaear-reporter/scripts/utils/stock_tools.py b/skills/alphaear-reporter/scripts/utils/stock_tools.py deleted file mode 100644 index 5929f74..0000000 --- a/skills/alphaear-reporter/scripts/utils/stock_tools.py +++ /dev/null @@ -1,257 +0,0 @@ -from datetime import datetime, timedelta -from typing import List, Dict, Optional -import akshare as ak -import pandas as pd -import re -import sqlite3 -from requests.exceptions import RequestException -from loguru import logger -from .database_manager import DatabaseManager -import os -from contextlib import contextmanager - -@contextmanager -def temporary_no_proxy(): - """Context manager to temporarily unset proxy environment variables.""" - proxies = {k: os.environ.get(k) for k in ['http_proxy', 'https_proxy', 'HTTP_PROXY', 'HTTPS_PROXY']} - for k in proxies: - if k in os.environ: - del os.environ[k] - try: - yield - finally: - for k, v in proxies.items(): - if v is not None: - os.environ[k] = v - -class StockTools: - """金融分析股票工具 - 结合高性能数据库缓存与增量更新""" - - def __init__(self, db: DatabaseManager, auto_update: bool = True): - """ - 初始化股票工具 - - Args: - db: 数据库管理器 - auto_update: 是否在列表为空时自动更新,默认 True - """ - self.db = db - if auto_update: - self._check_and_update_stock_list() - - def _check_and_update_stock_list(self, force: bool = False): - """检查并更新股票列表。仅在列表为空或 force=True 时从网络拉取。""" - # 直接查询表中记录数 - cursor = self.db.conn.cursor() - cursor.execute("SELECT COUNT(*) FROM stock_list") - count = cursor.fetchone()[0] - - if count > 0 and not force: - logger.info(f"ℹ️ Stock list already cached ({count} stocks)") - return - - logger.info("📡 Updating A-share and HK-share stock list from akshare...") - - def fetch_data(): - # A-share - df_a = ak.stock_zh_a_spot_em() - df_a = df_a[['代码', '名称']].copy() - df_a.columns = ['code', 'name'] - - # HK-share - df_hk = ak.stock_hk_spot_em() - df_hk = df_hk[['代码', '名称']].copy() - df_hk.columns = ['code', 'name'] - - # Combine - return pd.concat([df_a, df_hk], ignore_index=True) - - try: - try: - df_combined = fetch_data() - except (RequestException, Exception) as e: - if "Proxy" in str(e) or "proxy" in str(e): - logger.warning(f"⚠️ Proxy error detected: {e}. Retrying with proxy disabled...") - with temporary_no_proxy(): - df_combined = fetch_data() - else: - raise e - - self.db.save_stock_list(df_combined) - logger.info(f"✅ Cached {len(df_combined)} stocks (A-share + HK) to database.") - - except Exception as e: - logger.error(f"❌ Failed to sync stock list: {e}") - - - def search_ticker(self, query: str, limit: int = 5) -> List[Dict]: - """ - 模糊搜索 A 股股票代码或名称,支持常见缩写。 - """ - # 清洗后缀 (如 CATL.SZ -> CATL, 000001.SZ -> 000001) - clean_query = re.sub(r'\.(SZ|SH|HK|US)$', '', query, flags=re.IGNORECASE) - - # 常见缩写映射 - aliases = { - "CATL": "宁德时代", - "BYD": "比亚迪", - "TSLA": "特斯拉", - "Moutai": "贵州茅台", - "Tencent": "腾讯", - "Alibaba": "阿里巴巴", - "Meituan": "美团", - } - - search_query = aliases.get(clean_query.upper(), clean_query) - - # Robustness: if regex-like ticker code is embedded in query (e.g. "300364 中文在线"), try to extract it - if not search_query.isdigit(): - # Extract explicit 5-6 digit codes - match = re.search(r'\b(\d{5,6})\b', clean_query) - if match: - search_query = match.group(1) - - return self.db.search_stock(search_query, limit) - - def get_stock_price( - self, - ticker: str, - start_date: Optional[str] = None, - end_date: Optional[str] = None, - force_sync: bool = False, - ) -> pd.DataFrame: - """ - 获取指定股票的历史价格数据。优先从本地缓存读取,缺失时自动从网络补齐。 - - Args: - ticker: 股票代码,如 "600519"(贵州茅台)或 "000001"(平安银行)。 - start_date: 开始日期,格式 "YYYY-MM-DD"。默认为 90 天前。 - end_date: 结束日期,格式 "YYYY-MM-DD"。默认为今天。 - - Returns: - 包含 date, open, close, high, low, volume, change_pct 列的 DataFrame。 - """ - now = datetime.now() - if not end_date: - end_date = now.strftime('%Y-%m-%d') - if not start_date: - start_date = (now - timedelta(days=90)).strftime('%Y-%m-%d') - - df_db = self.db.get_stock_prices(ticker, start_date, end_date) - - need_update = False - if df_db.empty: - need_update = True - else: - db_latest = pd.to_datetime(df_db['date'].max()) - req_latest = pd.to_datetime(end_date) - if (req_latest - db_latest).days > 2: - need_update = True - - if force_sync: - need_update = True - - if need_update: - logger.info(f"📡 Data stale or missing for {ticker}, syncing from network...") - - # 清洗 ticker,确保只包含数字(Akshare A 股接口通常只需要数字代码) - clean_ticker = "".join(filter(str.isdigit, ticker)) - if not clean_ticker: - # Non A/H numeric tickers are not supported by the current data source. - logger.warning(f"⚠️ Unsupported ticker format (A/H only): {ticker}") - return df_db - - try: - s_fmt = start_date.replace("-", "") - e_fmt = end_date.replace("-", "") - - df_remote = None - - def fetch_data(): - if len(clean_ticker) == 5: - # HK Stock - return ak.stock_hk_hist( - symbol=clean_ticker, period="daily", - start_date=s_fmt, end_date=e_fmt, - adjust="qfq" - ) - else: - # A-share Stock - return ak.stock_zh_a_hist( - symbol=clean_ticker, period="daily", - start_date=s_fmt, end_date=e_fmt, - adjust="qfq" - ) - - try: - df_remote = fetch_data() - except (RequestException, Exception) as e: - if "Proxy" in str(e) or "proxy" in str(e): - logger.warning(f"⚠️ Proxy error detected: {e}. Retrying with proxy disabled...") - with temporary_no_proxy(): - df_remote = fetch_data() - else: - raise e - - if df_remote is not None and not df_remote.empty: - df_remote = df_remote.rename(columns={ - '日期': 'date', '开盘': 'open', '收盘': 'close', - '最高': 'high', '最低': 'low', '成交量': 'volume', - '涨跌幅': 'change_pct' - }) - # 确保日期格式正确 - df_remote['date'] = pd.to_datetime(df_remote['date']).dt.strftime('%Y-%m-%d') - - # 只有在获取到有意义的数据时才保存 - self.db.save_stock_prices(clean_ticker, df_remote) # 保存时使用清洗后的 clean_ticker - - # 重新查询数据库返回结果,保证一致性 - return self.db.get_stock_prices(clean_ticker, start_date, end_date) - else: - logger.warning(f"⚠️ Akshare returned empty data for {clean_ticker}") - - except KeyError as e: - # Akshare 有时在某些股票无数据时会抛出 KeyError - logger.warning(f"⚠️ Akshare data missing for {clean_ticker}: {e}") - except (RequestException, ConnectionError) as e: - logger.error(f"❌ Network error during Akshare sync for {clean_ticker}: {e}") - except sqlite3.Error as e: - logger.error(f"❌ Database error during Akshare sync for {clean_ticker}: {e}") - except Exception as e: - logger.error(f"❌ Unexpected error during Akshare sync for {clean_ticker}: {e}") - - return df_db - - -def get_stock_analysis(ticker: str, db: DatabaseManager) -> str: - """ - 生成指定股票的分析摘要报告。 - - Args: - ticker: 股票代码 - db: 数据库管理器实例 - - Returns: - Markdown 格式的分析报告,包含价格走势和关键指标。 - """ - tools = StockTools(db) - df = tools.get_stock_price(ticker) - - if df.empty: - return f"❌ 未能获取 {ticker} 的股价数据。" - - latest = df.iloc[-1] - change = ((latest['close'] - df.iloc[0]['close']) / df.iloc[0]['close']) * 100 - - report = [ - f"## 📊 {ticker} 分析报告", - f"- **查询时段**: {df.iloc[0]['date']} -> {latest['date']}", - f"- **当前价**: ¥{latest['close']:.2f}", - f"- **时段涨跌**: {change:+.2f}%", - f"- **最高/最低**: ¥{df['high'].max():.2f} / ¥{df['low'].min():.2f}", - "\n### 最近交易概览", - "```", - df.tail(5)[['date', 'close', 'change_pct', 'volume']].to_string(index=False), - "```" - ] - return "\n".join(report) diff --git a/skills/alphaear-reporter/scripts/visualizer.py b/skills/alphaear-reporter/scripts/visualizer.py deleted file mode 100644 index 85a38cd..0000000 --- a/skills/alphaear-reporter/scripts/visualizer.py +++ /dev/null @@ -1,472 +0,0 @@ -import os -from typing import Dict, List, Any, Optional -import pandas as pd -from loguru import logger -from pyecharts.charts import Kline, Line, Bar, Grid, Radar, Graph -from pyecharts import options as opts -from pyecharts.globals import ThemeType -from datetime import datetime, timedelta - -class VisualizerTools: - """可视化工具库 - 使用 Pyecharts 生成 HTML 图表""" - - @staticmethod - def generate_stock_chart( - df: pd.DataFrame, - ticker: str, - title: str = None, - prediction: Optional[List[float]] = None, - forecast: Optional[Any] = None, # ForecastResult instance - ground_truth: Optional[pd.DataFrame] = None # For training visualization - ) -> Grid: - """ - 生成股票 K 线图 + 成交量 + 预测趋势 (支持多状态 K 线) - """ - if df.empty: - return None - - # 数据预处理 - df = df.sort_values('date') - dates = [str(d)[:10] for d in df['date'].tolist()] - k_data = df[['open', 'close', 'low', 'high']].values.tolist() - volumes = df['volume'].tolist() - - if not title: - title = f"{ticker} 股价走势与预测" - - legend_items = ["日K"] - - # 1. 处理传统的简单预测线 (Line) - pred_line = None - if prediction and not forecast: - try: - last_date_str = dates[-1] - last_date = datetime.strptime(last_date_str, "%Y-%m-%d") - - pred_dates = [] - for i in range(1, len(prediction) + 1): - pred_dates.append((last_date + timedelta(days=i)).strftime("%Y-%m-%d")) - - ext_dates = dates + pred_dates - last_close = df.iloc[-1]['close'] - pred_values = [None] * (len(df) - 1) + [float(last_close)] + prediction - - pred_line = ( - Line() - .add_xaxis(ext_dates) - .add_yaxis( - "AI预测趋势", - pred_values, - is_connect_nones=True, - is_symbol_show=True, - linestyle_opts=opts.LineStyleOpts(width=2, type_="dashed", color="#FF8C00"), - label_opts=opts.LabelOpts(is_show=False) - ) - ) - dates = ext_dates - legend_items.append("AI预测趋势") - except Exception as e: - logger.error(f"Failed to process simple prediction: {e}") - - # 2. 处理复杂的 Kronos 预测 (Kline) - base_kline = None - adj_kline = None - - if forecast: - try: - # 获取预测数据点 - base_points = forecast.base_forecast # List[KLinePoint] - adj_points = forecast.adjusted_forecast # List[KLinePoint] - - # 提取日期 - pred_dates = [str(p.date)[:10] for p in (adj_points or base_points)] - - # 检查日期是否已经包含在主 dates 中,如果没有则扩展 - if pred_dates and pred_dates[0] not in dates: - dates = dates + pred_dates - - # 构建 Baseline 预测 K 线数据 - if base_points: - # 前面填充 None - base_k_data = [[None]*4] * len(df) + [[p.open, p.close, p.low, p.high] for p in base_points] - base_kline = ( - Kline() - .add_xaxis(dates) - .add_yaxis( - "模型原始预测", - base_k_data, - itemstyle_opts=opts.ItemStyleOpts( - color="transparent", - color0="transparent", - border_color="#FF8C00", # 橙色 - border_color0="#FF8C00", - opacity=0.6, - border_type="dashed" - ), - ) - ) - legend_items.append("模型原始预测") - - # 构建 Adjusted 调优 K 线数据 - if adj_points: - adj_k_data = [[None]*4] * len(df) + [[p.open, p.close, p.low, p.high] for p in adj_points] - adj_kline = ( - Kline() - .add_xaxis(dates) - .add_yaxis( - "LLM调优预测", - adj_k_data, - itemstyle_opts=opts.ItemStyleOpts( - color="#9333ea", # 紫色 - color0="#9333ea", - border_color="#9333ea", - border_color0="#9333ea", - opacity=0.8 - ), - ) - ) - legend_items.append("LLM调优预测") - - except Exception as e: - logger.error(f"Failed to process complex forecast: {e}") - - # 2.5 处理 Ground Truth (用于训练评估可视化) - gt_line = None - if ground_truth is not None and not ground_truth.empty: - try: - gt_dates = [str(d)[:10] for d in ground_truth['date'].tolist()] - # 确保日期包含在 dates 中 - for d in gt_dates: - if d not in dates: - dates.append(d) - dates = sorted(list(set(dates))) # Re-sort to maintain order - - gt_values = [None] * len(dates) - for _, row in ground_truth.iterrows(): - d_str = str(row['date'])[:10] - if d_str in dates: - idx = dates.index(d_str) - gt_values[idx] = float(row['close']) - - gt_line = ( - Line() - .add_xaxis(dates) - .add_yaxis( - "真实走势 (GT)", - gt_values, - is_connect_nones=True, - linestyle_opts=opts.LineStyleOpts(width=3, color="#2ecc71"), # 绿色粗线 - label_opts=opts.LabelOpts(is_show=False) - ) - ) - legend_items.append("真实走势 (GT)") - except Exception as e: - logger.error(f"Failed to process ground truth: {e}") - - # 3. 主 K 线图 - # 为了展示预测,也需要对主 K 线数据进行填充 - main_k_data = k_data + [[None]*4] * (len(dates) - len(df)) - - kline = ( - Kline() - .add_xaxis(dates) - .add_yaxis( - "日K", - main_k_data, - itemstyle_opts=opts.ItemStyleOpts( - color="#ef4444", # 跌 - color0="#22c55e", # 涨 - border_color="#ef4444", - border_color0="#22c55e", - ), - ) - .set_global_opts( - title_opts=opts.TitleOpts(title=title, pos_left="center"), - xaxis_opts=opts.AxisOpts(is_scale=True), - yaxis_opts=opts.AxisOpts( - is_scale=True, - splitarea_opts=opts.SplitAreaOpts( - is_show=True, areastyle_opts=opts.AreaStyleOpts(opacity=1) - ), - ), - legend_opts=opts.LegendOpts(is_show=True, pos_top="5%"), - datazoom_opts=[opts.DataZoomOpts(type_="inside", range_start=50)], - tooltip_opts=opts.TooltipOpts(trigger="axis", axis_pointer_type="cross"), - ) - ) - - # Overlap all series - if pred_line: kline.overlap(pred_line) - if base_kline: kline.overlap(base_kline) - if adj_kline: kline.overlap(adj_kline) - if gt_line: kline.overlap(gt_line) - - # 4. 成交量柱状图 - # 同理扩展成交量数据 - ext_volumes = volumes + [0] * (len(dates) - len(df)) - - bar = ( - Bar() - .add_xaxis(dates) - .add_yaxis( - "成交量", - ext_volumes, - xaxis_index=1, - yaxis_index=1, - label_opts=opts.LabelOpts(is_show=False), - itemstyle_opts=opts.ItemStyleOpts(color="#7fbe9e"), - ) - .set_global_opts( - xaxis_opts=opts.AxisOpts( - type_="category", - grid_index=1, - axislabel_opts=opts.LabelOpts(is_show=False), - ), - legend_opts=opts.LegendOpts(is_show=False), - ) - ) - - # 5. 组合 Grid - grid_chart = Grid(init_opts=opts.InitOpts(width="100%", height="450px", theme=ThemeType.LIGHT)) - grid_chart.add( - kline, - grid_opts=opts.GridOpts(pos_left="10%", pos_right="8%", height="50%"), - ) - grid_chart.add( - bar, - grid_opts=opts.GridOpts( - pos_left="10%", pos_right="8%", pos_top="65%", height="20%" - ), - ) - - return grid_chart - - @staticmethod - def generate_loss_chart(losses: List[float], title: str = "训练损失收敛曲线") -> Line: - """生成 Loss 下降曲线图""" - line = ( - Line(init_opts=opts.InitOpts(width="100%", height="400px", theme=ThemeType.LIGHT)) - .add_xaxis(list(range(1, len(losses) + 1))) - .add_yaxis( - "Training Loss", - losses, - is_smooth=True, - linestyle_opts=opts.LineStyleOpts(width=2, color="#3b82f6"), - label_opts=opts.LabelOpts(is_show=False), - markpoint_opts=opts.MarkPointOpts(data=[opts.MarkPointItem(type_="min", name="最小值")]) - ) - .set_global_opts( - title_opts=opts.TitleOpts(title=title, pos_left="center"), - xaxis_opts=opts.AxisOpts(name="Epoch", is_scale=True), - yaxis_opts=opts.AxisOpts(name="Loss", is_scale=True), - tooltip_opts=opts.TooltipOpts(trigger="axis"), - ) - ) - return line - - @staticmethod - def generate_sentiment_trend_chart(sentiment_history: List[Dict[str, Any]]) -> Line: - """ - 生成舆情情绪趋势图 - :param sentiment_history: [{"date": "2024-01-01", "score": 0.8}, ...] - """ - dates = [item['date'] for item in sentiment_history] - scores = [item['score'] for item in sentiment_history] - - line = ( - Line(init_opts=opts.InitOpts(width="100%", height="300px", theme=ThemeType.LIGHT)) - .add_xaxis(dates) - .add_yaxis( - "情绪指数", - scores, - is_smooth=True, - markline_opts=opts.MarkLineOpts(data=[opts.MarkLineItem(y=0, name="中性线")]), - itemstyle_opts=opts.ItemStyleOpts(color="#5470c6"), - areastyle_opts=opts.AreaStyleOpts(opacity=0.3, color="#5470c6") - ) - .set_global_opts( - title_opts=opts.TitleOpts(title="舆情情绪趋势", pos_left="center"), - legend_opts=opts.LegendOpts(pos_top="8%"), - yaxis_opts=opts.AxisOpts(min_=-1, max_=1, name="Sentiment"), - tooltip_opts=opts.TooltipOpts(trigger="axis"), - ) - ) - return line - - @staticmethod - def generate_isq_radar_chart(sentiment: float, confidence: float, intensity: int, - expectation_gap: float = 0.5, timeliness: float = 0.8, - title: str = "信号质量 ISQ 评估") -> Radar: - """生成信号质量雷达图""" - # 标准化数据 (0-100) - # sentiment 强度: 绝对值越大强度越高 - sent_val = min(100, abs(sentiment) * 100) - # confidence: 0 to 1 -> 0 to 100 - conf_val = confidence * 100 - # intensity: 1 to 5 -> 20 to 100 - int_val = intensity * 20 - # gap & time: 0 to 1 -> 0 to 100 - gap_val = expectation_gap * 100 - time_val = timeliness * 100 - - schema = [ - opts.RadarIndicatorItem(name="情绪强度", max_=100), - opts.RadarIndicatorItem(name="确定性", max_=100), - opts.RadarIndicatorItem(name="影响力", max_=100), - opts.RadarIndicatorItem(name="预期差", max_=100), - opts.RadarIndicatorItem(name="时效性", max_=100), - ] - - radar = ( - Radar(init_opts=opts.InitOpts(width="100%", height="400px", theme=ThemeType.LIGHT)) - .add_schema(schema=schema) - .add( - "信号特征", - [[sent_val, conf_val, int_val, gap_val, time_val]], - color="#f97316", - areastyle_opts=opts.AreaStyleOpts(opacity=0.3, color="#fb923c"), - ) - .set_global_opts( - title_opts=opts.TitleOpts(title=title, pos_left="center"), - legend_opts=opts.LegendOpts(is_show=False), - ) - ) - return radar - - @staticmethod - def generate_transmission_graph(nodes_data: List[Dict[str, str]], title: str = "投资逻辑传导链条") -> Graph: - """生成逻辑传导拓扑图 (支持分支结构)""" - nodes = [] - links = [] - - # Helper for text wrapping - def wrap_text(text, width=6): - return '\n'.join([text[i:i+width] for i in range(0, len(text), width)]) - - # Map original names to wrapped names to handle links - name_map = {} - - for i, item in enumerate(nodes_data): - # 节点样式 - color = "#ef4444" if "利空" in item.get("impact_type", "") else "#22c55e" - if "中性" in item.get("impact_type", ""): color = "#6b7280" - - original_name = item.get("node_name", f"节点{i}") - wrapped_name = wrap_text(original_name) - name_map[original_name] = wrapped_name - name_map[str(item.get("id", ""))] = wrapped_name # Map ID if present - - nodes.append({ - "name": wrapped_name, - "symbolSize": 60 if i == 0 else 50, - "value": item.get("logic", ""), - "itemStyle": {"color": color}, - # Improve label readability - "label": {"show": True, "formatter": "{b}"} - }) - - # Logic for Links - source_key = item.get("source") or item.get("parent") or item.get("parent_id") - if source_key: - # Branching logic: Link from specified source - # Source needs to be resolved to its (wrapped) name - target_source_name = name_map.get(source_key) - if not target_source_name and source_key in name_map.values(): - target_source_name = source_key # It was already a mapped name? - - # If we found the source in our map (meaning it appeared before this node) - if target_source_name: - links.append({"source": target_source_name, "target": wrapped_name}) - elif i > 0: - # Fallback: Linear chain - links.append({"source": nodes[i-1]["name"], "target": wrapped_name}) - - graph = ( - Graph(init_opts=opts.InitOpts(width="100%", height="400px", theme=ThemeType.LIGHT)) - .add( - "", - nodes, - links, - repulsion=5000, - layout="force", - is_roam=True, - is_draggable=True, - symbol="circle", - edge_symbol=['circle', 'arrow'], # Add arrows - edge_symbol_size=[4, 10], - linestyle_opts=opts.LineStyleOpts(width=2, curve=0.2, opacity=0.9), - label_opts=opts.LabelOpts(is_show=True, position="inside", color="white", font_size=10), - edge_label=opts.LabelOpts(is_show=False), - ) - .set_global_opts( - title_opts=opts.TitleOpts(title=title, pos_left="center"), - tooltip_opts=opts.TooltipOpts(formatter="{b}: {c}") - ) - ) - return graph - - @staticmethod - def render_drawio_to_html(xml_content: str, filename: str, title: str = "Logic Diagram") -> str: - """ - 将 Draw.io XML 渲染为包含 Viewer 的 HTML 文件 - """ - import json - - # 构造配置字典 - config = { - "highlight": "#0000ff", - "nav": True, - "resize": True, - "toolbar": "zoom", - "xml": xml_content - } - - # 1. 转为 JSON 字符串 (自动处理内部的引号转义、换行符转义等) - json_str = json.dumps(config) - - # 2. 转为 HTML 属性安全的字符串 (主要是转义单引号,因为我们在 HTML 中用单引号包裹) - import html - safe_json_str = html.escape(json_str, quote=True) - - html_template = f""" - - - - - {title} - - - -

{title}

-
- - - - """ - - try: - os.makedirs(os.path.dirname(filename), exist_ok=True) - # Use 'w' mode with utf-8 encoding - with open(filename, 'w', encoding='utf-8') as f: - f.write(html_template) - logger.info(f"✅ Draw.io chart rendered to {filename}") - return filename - except Exception as e: - logger.error(f"Failed to render drawio chart: {e}") - return "" - - @staticmethod - def render_chart_to_file(chart: Any, filename: str) -> str: - """渲染并保存 HTML""" - try: - # 确保目录存在 - os.makedirs(os.path.dirname(filename), exist_ok=True) - chart.render(filename) - logger.info(f"✅ Chart rendered to {filename}") - return filename - except Exception as e: - logger.error(f"Failed to render chart: {e}") - return "" diff --git a/skills/alphaear-reporter/tests/test_reporter.py b/skills/alphaear-reporter/tests/test_reporter.py deleted file mode 100644 index 191c4fc..0000000 --- a/skills/alphaear-reporter/tests/test_reporter.py +++ /dev/null @@ -1,29 +0,0 @@ -import sys -import os -import unittest - -# Add skill root to path -sys.path.append(os.path.abspath(os.path.join(os.path.dirname(__file__), '..'))) - -# try: -from scripts.visualizer import VisualizerTools -from scripts.report_agent import ReportAgent -from scripts.utils.database_manager import DatabaseManager -# except ImportError as e: -# print(f"Import Error: {e}") -# sys.exit(1) - -class TestReporter(unittest.TestCase): - def test_visualizer(self): - print("Testing Visualizer...") - viz = VisualizerTools() - self.assertIsNotNone(viz) - - def test_agent_init(self): - print("Testing ReportAgent...") - # Mocking or simplified init might be needed if agent requires extensive config - # Just checking import for now is a big win - pass - -if __name__ == '__main__': - unittest.main() diff --git a/skills/alphaear-search/SKILL.md b/skills/alphaear-search/SKILL.md deleted file mode 100644 index e1318ca..0000000 --- a/skills/alphaear-search/SKILL.md +++ /dev/null @@ -1,35 +0,0 @@ ---- -name: alphaear-search -description: Perform finance web searches and local context searches. Use when the user needs general finance info from the web (Jina/DDG/Baidu) or needs to retrieve finance information from a local document store (RAG). ---- - -# AlphaEar Search Skill - -## Overview - -Unified search capabilities: web search (Jina/DDG/Baidu) and local RAG search. - -## Capabilities - -### 1. Web Search - -Use `scripts/search_tools.py` via `SearchTools`. - -- **Search**: `search(query, engine, max_results)` - - Engines: `jina`, `ddg`, `baidu`, `local`. - - Returns: JSON string (summary) or List[Dict] (via `search_list`). -- **Smart Cache (Agentic)**: If you want to avoid redundant searches, use the **Search Cache Relevance Prompt** in `references/PROMPTS.md`. Read the cache first and decide if it's usable. -- **Aggregate**: `aggregate_search(query)` - - Combines results from multiple engines. - - -### 2. Local RAG - -Use `scripts/hybrid_search.py` or `SearchTools` with `engine='local'`. - -- **Search**: Searches local `daily_news` database. - -## Dependencies - -- `duckduckgo-search`, `requests` -- `scripts/database_manager.py` (search cache & local news) diff --git a/skills/alphaear-search/references/PROMPTS.md b/skills/alphaear-search/references/PROMPTS.md deleted file mode 100644 index f859eec..0000000 --- a/skills/alphaear-search/references/PROMPTS.md +++ /dev/null @@ -1,20 +0,0 @@ -# AlphaEar Search Prompts - -## Search Cache Relevance (Smart Cache) - -**Prompt:** - -```markdown -Task: Decide if existing information from previous searches or local news is sufficient for the new search query. - -New Query: "{current_query}" - -Available Information Candidates: -{candidates_desc} - -Instructions: -1. Analyze if any candidate provides ENOUGH up-to-date info for the "New Query". -2. If yes, choose the best one. -3. If the query implies needing LATEST real-time info and candidates are older than a few hours/days (depending on topic volatility), choose none. -4. Return strictly JSON: {"reuse": true/false, "index": , "reason": "short explanation"} -``` diff --git a/skills/alphaear-search/scripts/__init__.py b/skills/alphaear-search/scripts/__init__.py deleted file mode 100644 index e69de29..0000000 diff --git a/skills/alphaear-search/scripts/content_extractor.py b/skills/alphaear-search/scripts/content_extractor.py deleted file mode 100644 index 133207a..0000000 --- a/skills/alphaear-search/scripts/content_extractor.py +++ /dev/null @@ -1,122 +0,0 @@ -import requests -from requests.exceptions import RequestException, Timeout, ConnectionError -import os -import time -import json -import threading -from typing import Optional -from loguru import logger - - -class ContentExtractor: - """内容提取工具 - 主要接入 Jina Reader API""" - - JINA_BASE_URL = "https://r.jina.ai/" - - # 速率限制配置 (无 API Key 时:20 次/分钟) - _rate_limit_no_key = 20 # 每分钟最大请求数 - _rate_window = 60.0 # 时间窗口(秒) - _min_interval = 3.0 # 请求最小间隔(秒) - - # 类级别的速率限制状态 - _request_times = [] - _last_request_time = 0.0 - _lock = threading.Lock() - - @classmethod - def _wait_for_rate_limit(cls, has_api_key: bool) -> None: - """等待以满足速率限制要求""" - if has_api_key: - # 有 API Key 时,只需保持最小间隔 - time.sleep(0.5) - return - - with cls._lock: - current_time = time.time() - - # 1. 清理过期的请求记录 - cls._request_times = [t for t in cls._request_times if current_time - t < cls._rate_window] - - # 2. 检查是否达到速率限制 - if len(cls._request_times) >= cls._rate_limit_no_key: - # 需要等待最旧的请求过期 - oldest = cls._request_times[0] - wait_time = cls._rate_window - (current_time - oldest) + 1.0 - if wait_time > 0: - logger.warning(f"⏳ Jina rate limit reached, waiting {wait_time:.1f}s...") - time.sleep(wait_time) - current_time = time.time() - cls._request_times = [t for t in cls._request_times if current_time - t < cls._rate_window] - - # 3. 确保请求间隔不太快 - time_since_last = current_time - cls._last_request_time - if time_since_last < cls._min_interval: - sleep_time = cls._min_interval - time_since_last - time.sleep(sleep_time) - - # 4. 记录本次请求 - cls._request_times.append(time.time()) - cls._last_request_time = time.time() - - @classmethod - def extract_with_jina(cls, url: str, timeout: int = 30) -> Optional[str]: - """ - 使用 Jina Reader 提取网页正文内容 (Markdown 格式) - - 无 API Key 时自动限速:每分钟最多 20 次请求,每次间隔至少 3 秒 - """ - if not url or not url.startswith("http"): - return None - - logger.info(f"🕸️ Extracting content from: {url} via Jina...") - - headers = { - "User-Agent": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/131.0.0.0 Safari/537.36", - "Accept": "application/json" - } - - # 使用统一的 JINA_API_KEY - api_key = os.getenv("JINA_API_KEY") - has_api_key = bool(api_key and api_key.strip()) - - if has_api_key: - headers["Authorization"] = f"Bearer {api_key}" - - # 等待速率限制 - cls._wait_for_rate_limit(has_api_key) - - try: - # Jina Reader API - full_url = f"{cls.JINA_BASE_URL}{url}" - response = requests.get(full_url, headers=headers, timeout=timeout) - - if response.status_code == 200: - try: - data = response.json() - # Jina JSON 响应格式通常在 data.content - if isinstance(data, dict) and "data" in data: - return data["data"].get("content", "") - return data.get("content", response.text) - except (json.JSONDecodeError, TypeError): - return response.text - elif response.status_code == 429: - # 触发速率限制,等待后重试一次 - logger.warning(f"⚠️ Jina rate limit (429), waiting 60s before retry...") - time.sleep(60) - return cls.extract_with_jina(url, timeout) - else: - logger.warning(f"Jina extraction failed (Status {response.status_code}) for {url}") - return None - - except Timeout: - logger.error(f"Timeout during Jina extraction for {url}") - return None - except ConnectionError: - logger.error(f"Connection error during Jina extraction for {url}") - return None - except RequestException as e: - logger.error(f"Request error during Jina extraction: {e}") - return None - except Exception as e: - logger.error(f"Unexpected error during Jina extraction: {e}") - return None diff --git a/skills/alphaear-search/scripts/database_manager.py b/skills/alphaear-search/scripts/database_manager.py deleted file mode 100644 index 26b1ca9..0000000 --- a/skills/alphaear-search/scripts/database_manager.py +++ /dev/null @@ -1,159 +0,0 @@ -import sqlite3 -import json -from datetime import datetime -from pathlib import Path -from typing import List, Dict, Optional, Union -from loguru import logger - -class DatabaseManager: - """ - AlphaEar Search Database Manager - Reduced version for alphaear-search skill - """ - - def __init__(self, db_path: str = "data/signal_flux.db"): - self.db_path = Path(db_path) - self.db_path.parent.mkdir(parents=True, exist_ok=True) - self.conn = sqlite3.connect(str(self.db_path), check_same_thread=False) - self.conn.row_factory = sqlite3.Row - self._init_db() - logger.debug(f"💾 Search Database initialized at {self.db_path}") - - def _init_db(self): - cursor = self.conn.cursor() - - # 1. Daily News (Required for Local Search RAG) - cursor.execute(""" - CREATE TABLE IF NOT EXISTS daily_news ( - id TEXT PRIMARY KEY, - source TEXT, - rank INTEGER, - title TEXT, - url TEXT, - content TEXT, - publish_time TEXT, - crawl_time TEXT, - sentiment_score REAL, - analysis TEXT, - meta_data TEXT - ) - """) - - # 2. Search Cache - cursor.execute(""" - CREATE TABLE IF NOT EXISTS search_cache ( - query_hash TEXT PRIMARY KEY, - query TEXT, - engine TEXT, - results TEXT, - timestamp TEXT - ) - """) - - # 3. Search Details - cursor.execute(""" - CREATE TABLE IF NOT EXISTS search_detail ( - id TEXT, - query_hash TEXT, - rank INTEGER, - title TEXT, - url TEXT, - content TEXT, - publish_time TEXT, - crawl_time TEXT, - sentiment_score REAL, - source TEXT, - meta_data TEXT, - PRIMARY KEY (query_hash, id) - ) - """) - - cursor.execute("CREATE INDEX IF NOT EXISTS idx_search_cache_timestamp ON search_cache(timestamp)") - self.conn.commit() - - # --- Search Cache Operations --- - - def get_search_cache(self, query_hash: str, ttl_seconds: Optional[int] = None) -> Optional[Dict]: - cursor = self.conn.cursor() - - # Try detailed cache first - cursor.execute(""" - SELECT * FROM search_detail - WHERE query_hash = ? - ORDER BY rank - """, (query_hash,)) - details = [dict(row) for row in cursor.fetchall()] - - if details: - first_time = datetime.fromisoformat(details[0]['crawl_time']) - if ttl_seconds and (datetime.now() - first_time).total_seconds() > ttl_seconds: - return None - return {"results": json.dumps(details), "timestamp": details[0]['crawl_time']} - - # Fallback to simple cache - cursor.execute("SELECT * FROM search_cache WHERE query_hash = ?", (query_hash,)) - row = cursor.fetchone() - - if not row: return None - row_dict = dict(row) - if ttl_seconds: - cache_time = datetime.fromisoformat(row_dict['timestamp']) - if (datetime.now() - cache_time).total_seconds() > ttl_seconds: - return None - return row_dict - - def save_search_cache(self, query_hash: str, query: str, engine: str, results: Union[str, List[Dict]]): - cursor = self.conn.cursor() - current_time = datetime.now().isoformat() - results_str = results if isinstance(results, str) else json.dumps(results) - - cursor.execute(""" - INSERT OR REPLACE INTO search_cache (query_hash, query, engine, results, timestamp) - VALUES (?, ?, ?, ?, ?) - """, (query_hash, query, engine, results_str, current_time)) - - if isinstance(results, list): - for item in results: - try: - item_id = item.get('id') or f"{hash(item.get('url', ''))}" - cursor.execute(""" - INSERT OR REPLACE INTO search_detail - (id, query_hash, rank, title, url, content, publish_time, crawl_time, sentiment_score, source, meta_data) - VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?) - """, ( - str(item_id), query_hash, item.get('rank', 0), item.get('title'), - item.get('url'), item.get('content', ''), item.get('publish_time'), - item.get('crawl_time') or current_time, item.get('sentiment_score'), - item.get('source'), json.dumps(item.get('meta_data', {})) - )) - except Exception as e: - logger.error(f"Error saving search detail: {e}") - - self.conn.commit() - - def find_similar_queries(self, query: str, limit: int = 5) -> List[Dict]: - cursor = self.conn.cursor() - q_wild = f"%{query}%" - cursor.execute(""" - SELECT query, query_hash, timestamp, results - FROM search_cache - WHERE query LIKE ? OR ? LIKE ('%' || query || '%') - ORDER BY timestamp DESC - LIMIT ? - """, (q_wild, query, limit)) - return [dict(row) for row in cursor.fetchall()] - - def search_local_news(self, query: str, limit: int = 5) -> List[Dict]: - cursor = self.conn.cursor() - q_wild = f"%{query}%" - cursor.execute(""" - SELECT * FROM daily_news - WHERE title LIKE ? OR content LIKE ? - ORDER BY crawl_time DESC - LIMIT ? - """, (q_wild, q_wild, limit)) - return [dict(row) for row in cursor.fetchall()] - - def close(self): - if self.conn: - self.conn.close() diff --git a/skills/alphaear-search/scripts/hybrid_search.py b/skills/alphaear-search/scripts/hybrid_search.py deleted file mode 100644 index c597fee..0000000 --- a/skills/alphaear-search/scripts/hybrid_search.py +++ /dev/null @@ -1,216 +0,0 @@ -import numpy as np -import os -from typing import List, Dict, Any, Optional, Union -from rank_bm25 import BM25Okapi -from loguru import logger -from sentence_transformers import SentenceTransformer -from sklearn.metrics.pairwise import cosine_similarity - -class HybridSearcher: - """ - 统一混合检索引擎 (Hybrid RAG) - 实现 BM25 (文本) + 向量 (语义) 的融合搜索 (RRF) - """ - - def __init__(self, data: List[Dict[str, Any]], text_fields: List[str] = ["title", "content"], model_name: str = None): - """ - 初始化搜索器 - - Args: - data: 数据列表,每个元素为 Dict - text_fields: 用于建立索引的文本字段 - model_name: 向量模型名称,默认使用 paraphrase-multilingual-MiniLM-L12-v2 - """ - self.data = data - self.text_fields = text_fields - self._corpus = [] - self._bm25 = None - self._vector_model = None - self._embeddings = None - self._fitted = False - self._vector_fitted = False - - # 默认模型 - self.model_name = model_name or os.getenv("EMBEDDING_MODEL", "paraphrase-multilingual-MiniLM-L12-v2") - - if data: - self._prepare_corpus() - self._fit_bm25() - # 延迟加载向量模型,仅在需要时或初始化时显式调用 - # self._fit_vector() - - def _prepare_corpus(self): - """准备语料库用于分词""" - import jieba # 使用 jieba 进行中文分词 - - self._corpus = [] - self._full_texts = [] - for item in self.data: - text = " ".join([str(item.get(field, "")) for field in self.text_fields]) - self._full_texts.append(text) - # 中文分词优化 - tokens = list(jieba.cut(text)) - self._corpus.append(tokens) - - def _fit_bm25(self): - """训练 BM25 模型""" - if self._corpus: - self._bm25 = BM25Okapi(self._corpus) - self._fitted = True - logger.info(f"✅ BM25 index fitted with {len(self.data)} documents") - - def _fit_vector(self): - """训练向量模型并生成 Embeddings""" - if not self.data: - return - - try: - logger.info(f"📡 Loading embedding model: {self.model_name}...") - self._vector_model = SentenceTransformer(self.model_name) - logger.info(f"🧠 Encoding {len(self._full_texts)} documents...") - self._embeddings = self._vector_model.encode(self._full_texts, show_progress_bar=False) - self._vector_fitted = True - logger.info("✅ Vector index fitted successfully") - except Exception as e: - logger.error(f"❌ Failed to fit vector index: {e}") - self._vector_fitted = False - - def _compute_rrf(self, rank_lists: List[List[int]], k: int = 60) -> List[tuple]: - """ - 计算 Reciprocal Rank Fusion (RRF) - - Args: - rank_lists: 多个排序后的索引列表 - k: RRF 常数,默认 60 - """ - scores = {} - for rank_list in rank_lists: - for rank, idx in enumerate(rank_list): - if idx not in scores: - scores[idx] = 0 - scores[idx] += 1.0 / (k + rank + 1) - - # 按分数排序 - sorted_indices = sorted(scores.items(), key=lambda x: x[1], reverse=True) - return sorted_indices - - def search(self, query: str, top_n: int = 5, use_vector: bool = False) -> List[Dict[str, Any]]: - """ - 执行混合搜索 - - Args: - query: 搜索关键词 - top_n: 返回结果数量 - use_vector: 是否启用向量搜索 - """ - if not self._fitted or not query: - return [] - - import jieba - query_tokens = list(jieba.cut(query)) - - # 1. BM25 搜索结果 - bm25_scores = self._bm25.get_scores(query_tokens) - bm25_rank = np.argsort(bm25_scores)[::-1].tolist() - - rank_lists = [bm25_rank] - - # 2. 向量搜索逻辑 - if use_vector: - if not self._vector_fitted: - self._fit_vector() - - if self._vector_fitted: - query_embedding = self._vector_model.encode([query], show_progress_bar=False) - similarities = cosine_similarity(query_embedding, self._embeddings)[0] - vector_rank = np.argsort(similarities)[::-1].tolist() - rank_lists.append(vector_rank) - else: - logger.warning("Vector search requested but model not fitted, falling back to BM25") - - # 3. 融合排序 (RRF) - if len(rank_lists) > 1: - rrf_results = self._compute_rrf(rank_lists) - # RRF 返回 (idx, score) 列表 - final_rank = [idx for idx, score in rrf_results] - else: - final_rank = bm25_rank - - # 返回前 top_n 条结果 - results = [self.data[idx].copy() for idx in final_rank[:top_n]] - - # 为每个结果注入相关性评分 - for i, res in enumerate(results): - try: - original_idx = final_rank[i] - res["_search_score"] = bm25_scores[original_idx] - if use_vector and self._vector_fitted: - res["_vector_score"] = float(similarities[original_idx]) - except: - res["_search_score"] = 0 - - return results - -class InMemoryRAG(HybridSearcher): - """专门用于 ReportAgent 跨章节检索的内存态 RAG""" - - def search(self, query: str, top_n: int = 3, use_vector: bool = True) -> List[Dict[str, Any]]: - """默认开启向量搜索的内存检索""" - return super().search(query, top_n=top_n, use_vector=use_vector) - - def update_data(self, new_data: List[Dict[str, Any]]): - """动态更新数据并重新训练索引""" - self.data = new_data - self._prepare_corpus() - self._fit_bm25() - # 如果之前已经加载过向量模型,则更新向量索引 - if self._vector_model: - self._fit_vector() - logger.info(f"🔄 InMemoryRAG updated with {len(new_data)} items") - -class LocalNewsSearch(HybridSearcher): - """持久态 RAG:检索数据库中的历史新闻""" - - def __init__(self, db_manager): - """ - Args: - db_manager: DatabaseManager 实例 - """ - self.db = db_manager - # 初始时不加载数据,需调用 load_history - super().__init__([], ["title", "content"]) - - def load_history(self, days: int = 30, limit: int = 1000): - """从数据库加载最近 N 天的新闻构建索引""" - try: - # 假设 db_manager 有 execute_query - query = f"SELECT title, content, publish_time, source FROM daily_news ORDER BY publish_time DESC LIMIT ?" - results = self.db.execute_query(query, (limit,)) - - data = [] - for row in results: - # 转换 Row 为 Dict - if hasattr(row, 'keys'): - item = dict(row) - else: - item = { - "title": row[0], - "content": row[1], - "publish_time": row[2], - "source": row[3] - } - data.append(item) - - self.data = data - self._prepare_corpus() - self._fit_bm25() - # 默认不立即训练向量,等到第一次搜索时按需训练 - logger.info(f"📚 LocalNewsSearch loaded {len(data)} items from history") - except Exception as e: - logger.error(f"Failed to load history for search: {e}") - - def search(self, query: str, top_n: int = 5, use_vector: bool = True) -> List[Dict[str, Any]]: - """执行本地历史搜索,默认开启向量搜索""" - if not self.data: - self.load_history() - return super().search(query, top_n=top_n, use_vector=use_vector) diff --git a/skills/alphaear-search/scripts/llm/__init__.py b/skills/alphaear-search/scripts/llm/__init__.py deleted file mode 100644 index e69de29..0000000 diff --git a/skills/alphaear-search/scripts/llm/capability.py b/skills/alphaear-search/scripts/llm/capability.py deleted file mode 100644 index d3fb2d7..0000000 --- a/skills/alphaear-search/scripts/llm/capability.py +++ /dev/null @@ -1,85 +0,0 @@ -import os -from typing import Optional, List, Dict, Any -from agno.agent import Agent -from agno.models.base import Model -from loguru import logger -from .factory import get_model - - -def test_tool_call_support(model: Model) -> bool: - """ - 测试模型是否支持原生的 Tool Call (Function Calling)。 - 通过尝试执行一个简单的加法工具来验证。 - """ - - def get_current_weather(location: str): - """获取指定地点的天气""" - return f"{location} 的天气是晴天,25度。" - - test_agent = Agent( - model=model, - tools=[get_current_weather], - instructions="请调用工具查询北京的天气,并直接返回工具的输出结果。", - ) - - try: - # 运行一个简单的任务,观察是否触发了 tool_call - response = test_agent.run("北京天气怎么样?") - - # 检查 response 中是否包含 tool_calls - # Agno 的 RunResponse 对象通常包含 messages,我们可以检查最后几条消息 - has_tool_call = False - for msg in response.messages: - if hasattr(msg, "tool_calls") and msg.tool_calls: - has_tool_call = True - break - - if has_tool_call: - logger.info(f"✅ Model {model.id} supports native tool calling.") - return True - else: - # 如果没有 tool_calls 但返回了正确答案,可能是模型通过纯文本模拟了工具调用(ReAct) - # 或者根本没用工具。对于原生支持的判断,我们坚持要求有 tool_calls 结构。 - logger.warning( - f"⚠️ Model {model.id} did NOT use native tool calling structure." - ) - return False - - except Exception as e: - logger.error(f"❌ Error testing tool call for {model.id}: {e}") - return False - - -class ModelCapabilityRegistry: - """ - 模型能力注册表,用于缓存和管理不同模型的能力测试结果。 - """ - - _cache = {} - - @classmethod - def get_capabilities( - cls, provider: str, model_id: str, **kwargs - ) -> Dict[str, bool]: - key = f"{provider}:{model_id}" - if key not in cls._cache: - logger.info(f"🔍 Testing capabilities for {key}...") - model = get_model(provider, model_id, **kwargs) - supports_tool_call = test_tool_call_support(model) - cls._cache[key] = {"supports_tool_call": supports_tool_call} - return cls._cache[key] - - -if __name__ == "__main__": - import os - from skills._env_loader import load_unified_env - - load_unified_env() - - # 测试当前配置的模型 - p = os.getenv("LLM_PROVIDER", "minimax") - m = os.getenv("LLM_MODEL", "Qwen") - - print(f"Testing {p}/{m}...") - res = ModelCapabilityRegistry.get_capabilities(p, m) - print(f"Result: {res}") diff --git a/skills/alphaear-search/scripts/llm/factory.py b/skills/alphaear-search/scripts/llm/factory.py deleted file mode 100644 index 09b6ea5..0000000 --- a/skills/alphaear-search/scripts/llm/factory.py +++ /dev/null @@ -1,114 +0,0 @@ -import os -from agno.models.openai import OpenAIChat -from agno.models.ollama import Ollama -from agno.models.dashscope import DashScope -from agno.models.deepseek import DeepSeek -from agno.models.openrouter import OpenRouter - -def get_model(model_provider: str, model_id: str, **kwargs): - """ - Factory to get the appropriate LLM model. - - Args: - model_provider: "openai", "ollama", "deepseek" - model_id: The specific model ID (e.g., "gpt-4o", "llama3", "deepseek-chat") - **kwargs: Additional arguments for the model constructor - """ - if model_provider == "openai": - return OpenAIChat(id=model_id, **kwargs) - - elif model_provider == "ollama": - return Ollama(id=model_id, **kwargs) - - elif model_provider == "deepseek": - # DeepSeek is OpenAI compatible - api_key = os.getenv("DEEPSEEK_API_KEY") - if not api_key: - print("Warning: DEEPSEEK_API_KEY not set.") - - return DeepSeek( - id=model_id, - api_key=api_key, - **kwargs - ) - elif model_provider == "dashscope": - api_key = os.getenv("DASHSCOPE_API_KEY") - if not api_key: - print("Warning: DASHSCOPE_API_KEY not set.") - - return DashScope( - id=model_id, - base_url="https://dashscope.aliyuncs.com/compatible-mode/v1", - api_key=api_key, - **kwargs - ) - elif model_provider == 'openrouter': - api_key = os.getenv("OPENROUTER_API_KEY") - if not api_key: - print('Warning: OPENROUTER_API_KEY not set.') - - return OpenRouter( - id=model_id, - api_key=api_key, - **kwargs - ) - - elif model_provider == 'zai': - api_key = os.getenv("ZAI_KEY_API") - if not api_key: - print('Warning: ZAI_KEY_API not set.') - - # role_map to ensure compatibility. - default_role_map = { - "system": "system", - "user": "user", - "assistant": "assistant", - "tool": "tool", - "model": "assistant", - } - - # Allow callers to override role_map via kwargs, otherwise use default - role_map = kwargs.pop("role_map", default_role_map) - - return OpenAIChat( - id=model_id, - base_url="https://api.z.ai/api/paas/v4", - api_key=api_key, - timeout=60, - role_map=role_map, - extra_body={"enable_thinking": False}, # TODO: one more setting for thinking - **kwargs - ) - - elif model_provider == 'ust': - api_key = os.getenv("UST_KEY_API") - if not api_key: - print('Warning: UST_KEY_API not set.') - - # Some UST-compatible endpoints expect the standard OpenAI role names - # (e.g. "system", "user", "assistant") rather than Agno's default - # mapping which maps "system" -> "developer". Provide an explicit - # role_map to ensure compatibility. - default_role_map = { - "system": "system", - "user": "user", - "assistant": "assistant", - "tool": "tool", - "model": "assistant", - } - - # Allow callers to override role_map via kwargs, otherwise use default - role_map = kwargs.pop("role_map", default_role_map) - - return OpenAIChat( - id=model_id, - api_key=api_key, - base_url=os.getenv("UST_URL"), - role_map=role_map, - extra_body={"enable_thinking": False}, # TODO: one more setting for thinking - **kwargs - ) - - else: - raise ValueError(f"Unknown model provider: {model_provider}") - diff --git a/skills/alphaear-search/scripts/llm/router.py b/skills/alphaear-search/scripts/llm/router.py deleted file mode 100644 index 20e7d83..0000000 --- a/skills/alphaear-search/scripts/llm/router.py +++ /dev/null @@ -1,80 +0,0 @@ -import os -from typing import Optional, List, Dict, Any, Union -from agno.models.base import Model -from loguru import logger -from .factory import get_model -from .capability import ModelCapabilityRegistry -from skills._env_loader import load_unified_env - -load_unified_env() - - -class ModelRouter: - """ - 模型路由管理器 - - 功能: - 1. 管理“推理/写作模型” (Reasoning Model) 和“工具调用模型” (Tool Model)。 - 2. 根据任务需求自动选择合适的模型。 - """ - - def __init__(self): - # 默认从环境变量读取 - self.reasoning_provider = os.getenv( - "REASONING_MODEL_PROVIDER", os.getenv("LLM_PROVIDER", "openai") - ) - self.reasoning_id = os.getenv( - "REASONING_MODEL_ID", os.getenv("LLM_MODEL", "gpt-4o") - ) - self.reasoning_host = os.getenv("REASONING_MODEL_HOST", os.getenv("LLM_HOST")) - - self.tool_provider = os.getenv("TOOL_MODEL_PROVIDER", self.reasoning_provider) - self.tool_id = os.getenv("TOOL_MODEL_ID", self.reasoning_id) - self.tool_host = os.getenv("TOOL_MODEL_HOST", self.reasoning_host) - - self._reasoning_model = None - self._tool_model = None - - logger.info( - f"🤖 ModelRouter initialized: Reasoning={self.reasoning_id} ({self.reasoning_host or 'default'}), Tool={self.tool_id} ({self.tool_host or 'default'})" - ) - - def get_reasoning_model(self, **kwargs) -> Model: - if not self._reasoning_model: - # 优先使用路由配置的 host - if self.reasoning_host and "host" not in kwargs: - kwargs["host"] = self.reasoning_host - self._reasoning_model = get_model( - self.reasoning_provider, self.reasoning_id, **kwargs - ) - return self._reasoning_model - - def get_tool_model(self, **kwargs) -> Model: - if not self._tool_model: - # 优先使用路由配置的 host - if self.tool_host and "host" not in kwargs: - kwargs["host"] = self.tool_host - - # 检查 tool_model 是否真的支持 tool call - caps = ModelCapabilityRegistry.get_capabilities( - self.tool_provider, self.tool_id, **kwargs - ) - if not caps["supports_tool_call"]: - logger.warning( - f"⚠️ Configured tool model {self.tool_id} might not support native tool calls! Consider using ReAct mode or a different model." - ) - - self._tool_model = get_model(self.tool_provider, self.tool_id, **kwargs) - return self._tool_model - - def get_model_for_agent(self, has_tools: bool = False, **kwargs) -> Model: - """ - 根据 Agent 是否包含工具来返回合适的模型。 - """ - if has_tools: - return self.get_tool_model(**kwargs) - return self.get_reasoning_model(**kwargs) - - -# 全局单例 -router = ModelRouter() diff --git a/skills/alphaear-search/scripts/search_tools.py b/skills/alphaear-search/scripts/search_tools.py deleted file mode 100644 index ea83bfd..0000000 --- a/skills/alphaear-search/scripts/search_tools.py +++ /dev/null @@ -1,479 +0,0 @@ -import os -import hashlib -import json -import re -import requests -import time -import threading -from typing import List, Dict, Optional, Any -from agno.tools.duckduckgo import DuckDuckGoTools -from agno.tools.baidusearch import BaiduSearchTools -from datetime import datetime -from .database_manager import DatabaseManager -from .content_extractor import ContentExtractor -from .hybrid_search import LocalNewsSearch - -# 默认搜索缓存 TTL(秒),可通过环境变量覆盖 -DEFAULT_SEARCH_TTL = int(os.getenv("SEARCH_CACHE_TTL", "3600")) # 默认 1 小时 - - -class JinaSearchEngine: - """Jina Search API 封装 - 使用 s.jina.ai 进行网络搜索""" - - JINA_SEARCH_URL = "https://s.jina.ai/" - - # 速率限制配置 - _rate_limit_no_key = 10 # 无 key 时每分钟最大请求数 - _rate_window = 60.0 - _min_interval = 2.0 - _request_times = [] - _last_request_time = 0.0 - _lock = threading.Lock() - - def __init__(self): - self.api_key = os.getenv("JINA_API_KEY", "").strip() - self.has_api_key = bool(self.api_key) - if self.has_api_key: - logger.info("✅ Jina Search API key configured") - - @classmethod - def _wait_for_rate_limit(cls, has_api_key: bool) -> None: - """等待以满足速率限制""" - if has_api_key: - time.sleep(0.3) - return - - with cls._lock: - current_time = time.time() - cls._request_times = [t for t in cls._request_times if current_time - t < cls._rate_window] - - if len(cls._request_times) >= cls._rate_limit_no_key: - oldest = cls._request_times[0] - wait_time = cls._rate_window - (current_time - oldest) + 1.0 - if wait_time > 0: - logger.warning(f"⏳ Jina Search rate limit, waiting {wait_time:.1f}s...") - time.sleep(wait_time) - current_time = time.time() - cls._request_times = [t for t in cls._request_times if current_time - t < cls._rate_window] - - time_since_last = current_time - cls._last_request_time - if time_since_last < cls._min_interval: - time.sleep(cls._min_interval - time_since_last) - - cls._request_times.append(time.time()) - cls._last_request_time = time.time() - - def search(self, query: str, max_results: int = 5) -> List[Dict]: - """ - 使用 Jina Search API 执行搜索 - - Args: - query: 搜索关键词 - max_results: 返回结果数量 - - Returns: - 搜索结果列表,每个结果包含 title, url, content - """ - if not query: - return [] - - logger.info(f"🔍 Jina Search: {query}") - - # 等待速率限制 - self._wait_for_rate_limit(self.has_api_key) - - headers = { - "Accept": "application/json", - "X-Retain-Images": "none", - } - - if self.has_api_key: - headers["Authorization"] = f"Bearer {self.api_key}" - - try: - # Jina Search API: https://s.jina.ai/{query} - import urllib.parse - encoded_query = urllib.parse.quote(query) - url = f"{self.JINA_SEARCH_URL}{encoded_query}" - - response = requests.get(url, headers=headers, timeout=30) - - if response.status_code == 429: - logger.warning("⚠️ Jina Search rate limited (429), waiting 30s...") - time.sleep(30) - return self.search(query, max_results) - - if response.status_code != 200: - logger.warning(f"Jina Search failed (Status {response.status_code})") - return [] - - # 解析响应 - try: - data = response.json() - except json.JSONDecodeError: - # 如果返回纯文本,尝试解析 - data = {"data": [{"title": "Search Result", "url": "", "content": response.text}]} - - results = [] - - # Jina 返回格式可能是 {"data": [...]} 或直接是列表 - items = data.get("data", []) if isinstance(data, dict) else data - if not isinstance(items, list): - items = [items] if items else [] - - for i, item in enumerate(items[:max_results]): - if isinstance(item, dict): - results.append({ - "title": item.get("title", f"Result {i+1}"), - "url": item.get("url", ""), - "href": item.get("url", ""), # 兼容性 - "content": item.get("content", item.get("description", "")), - "body": item.get("content", item.get("description", "")), # 兼容性 - }) - elif isinstance(item, str): - results.append({ - "title": f"Result {i+1}", - "url": "", - "content": item - }) - - logger.info(f"✅ Jina Search returned {len(results)} results") - return results - - except requests.exceptions.Timeout: - logger.error("Jina Search timeout") - return [] - except requests.exceptions.RequestException as e: - logger.error(f"Jina Search request error: {e}") - return [] - except Exception as e: - logger.error(f"Jina Search unexpected error: {e}") - return [] - -class SearchTools: - """扩展性搜索工具库 - 支持多引擎聚合与内容缓存""" - - def __init__(self, db: DatabaseManager): - self.db = db - - # 检查 Jina API Key 是否配置 - jina_api_key = os.getenv("JINA_API_KEY", "").strip() - self._jina_enabled = bool(jina_api_key) - - self._engines = { - "ddg": DuckDuckGoTools(), - "baidu": BaiduSearchTools(), - "local": LocalNewsSearch(db) - } - - # 如果配置了 Jina API Key,添加 Jina 引擎 - if self._jina_enabled: - self._engines["jina"] = JinaSearchEngine() - logger.info("🚀 Jina Search engine enabled (JINA_API_KEY configured)") - - # 确定默认搜索引擎 - self._default_engine = "jina" if self._jina_enabled else "ddg" - - def _generate_hash(self, query: str, engine: str, max_results: int) -> str: - return hashlib.md5(f"{engine}:{query}:{max_results}".encode()).hexdigest() - - def search(self, query: str, engine: str = None, max_results: int = 5, ttl: Optional[int] = None) -> str: - """ - 使用指定搜索引擎执行网络搜索,结果会被缓存以提高效率。 - - Args: - query: 搜索关键词,如 "英伟达财报" 或 "光伏行业政策"。 - engine: 搜索引擎选择。可选值: - "jina" (Jina Search,需配置 JINA_API_KEY,LLM友好输出), - "ddg" (DuckDuckGo,推荐英文/国际搜索), - "baidu" (百度,推荐中文/国内搜索), - "local" (本地历史新闻搜索,基于向量+BM25)。 - 默认: 若配置了 JINA_API_KEY 则使用 "jina",否则 "ddg"。 - max_results: 期望返回的结果数量,默认 5 条。 - ttl: 缓存有效期(秒)。如果缓存超过此时间会重新搜索。 - 默认使用环境变量 SEARCH_CACHE_TTL 或 3600 秒。 - 设为 0 可强制刷新。 - - Returns: - 搜索结果的文本描述,包含标题、摘要和链接。 - """ - # 使用默认引擎(如果配置了 Jina 则优先使用 Jina) - if engine is None: - engine = self._default_engine - - if engine not in self._engines: - return f"Error: Unsupported engine '{engine}'. Available: {list(self._engines.keys())}" - - query_hash = self._generate_hash(query, engine, max_results) - effective_ttl = ttl if ttl is not None else DEFAULT_SEARCH_TTL - - # 1. 尝试从缓存读取 (local 引擎不缓存,因为它本身就是查库) - if engine != "local": - cache = self.db.get_search_cache(query_hash, ttl_seconds=effective_ttl if effective_ttl > 0 else None) - if cache and effective_ttl != 0: - logger.info(f"ℹ️ Found search results in cache for: {query} ({engine})") - return cache['results'] - - # 2. 执行真实搜索 - logger.info(f"📡 Searching {engine} for: {query}") - try: - tool = self._engines[engine] - if engine == "jina": - # Jina Search 返回 List[Dict] - jina_results = tool.search(query, max_results=max_results) - results = [] - for r in jina_results: - results.append({ - "title": r.get("title", ""), - "href": r.get("url", ""), - "body": r.get("content", "") - }) - elif engine == "ddg": - results = tool.duckduckgo_search(query, max_results=max_results) - elif engine == "baidu": - results = tool.baidu_search(query, max_results=max_results) - elif engine == "local": - # LocalNewsSearch 返回的是 List[Dict] - local_results = tool.search(query, top_n=max_results) - results = [] - for r in local_results: - results.append({ - "title": r.get("title"), - "href": r.get("url", "local"), - "body": r.get("content", "") - }) - else: - results = "Search not implemented for this engine." - - results_str = str(results) - if engine != "local": - self.db.save_search_cache(query_hash, query, engine, results_str) - return results_str - - except Exception as e: - # 搜索失败时的降级策略 - if engine == "jina": - logger.warning(f"⚠️ Jina search failed, falling back to ddg: {query} ({e})") - try: - return self.search(query, engine="ddg", max_results=max_results, ttl=ttl) - except Exception as e2: - logger.error(f"❌ DDG fallback also failed for {query}: {e2}") - elif engine == "ddg": - logger.warning(f"⚠️ DDG search failed, falling back to baidu: {query} ({e})") - try: - return self.search(query, engine="baidu", max_results=max_results, ttl=ttl) - except Exception as e2: - logger.error(f"❌ Baidu fallback also failed for {query}: {e2}") - - logger.error(f"❌ Search failed for {query}: {e}") - return f"Error occurred during search: {str(e)}" - - def search_list(self, query: str, engine: str = None, max_results: int = 5, ttl: Optional[int] = None, enrich: bool = True) -> List[Dict]: - """ - 执行搜索并返回结构化列表 (List[Dict])。 - Dict 包含: title, href (or url), body (or snippet) - - Args: - engine: 搜索引擎,默认使用配置的默认引擎(Jina 优先) - enrich: 是否抓取正文内容 (默认 True) - """ - # 使用默认引擎 - if engine is None: - engine = self._default_engine - - if engine not in self._engines: - logger.error(f"Unsupported engine {engine}") - return [] - - # 不同的 hash 以区分是否 enrichment - enrich_suffix = ":enriched" if enrich else "" - query_hash = self._generate_hash(query, engine + enrich_suffix, max_results) - effective_ttl = ttl if ttl is not None else DEFAULT_SEARCH_TTL - - # 1. 尝试从缓存读取 - cache = self.db.get_search_cache(query_hash, ttl_seconds=effective_ttl if effective_ttl > 0 else None) - if cache and effective_ttl != 0: - try: - cached_data = json.loads(cache['results']) - if isinstance(cached_data, list): - logger.info(f"ℹ️ Found structured search cache for: {query}") - return cached_data - except: - pass - - # 1.5 Smart Cache (Delegated to Agent) - # The Agent should call list_similar_searches and judge relevance using PROMPTS.md - - - # 2. 执行搜索 - logger.info(f"📡 Searching {engine} (structured) for: {query}") - try: - tool = self._engines[engine] - results = [] - if engine == "jina": - # Jina Search 直接返回结构化数据 - jina_results = tool.search(query, max_results=max_results) - for r in jina_results: - results.append({ - "title": r.get("title", ""), - "url": r.get("url", ""), - "href": r.get("url", ""), - "body": r.get("content", ""), - "content": r.get("content", ""), - "source": "Jina Search" - }) - elif engine == "ddg": - results = tool.duckduckgo_search(query, max_results=max_results) - elif engine == "baidu": - results = tool.baidu_search(query, max_results=max_results) - elif engine == "local": - # LocalNewsSearch 返回的是 List[Dict] - local_results = tool.search(query, top_n=max_results) - results = [] - for r in local_results: - results.append({ - "title": r.get("title"), - "url": r.get("url", "local"), - "body": r.get("content", "")[:500], - "source": f"Local ({r.get('source', 'db')})", - "publish_time": r.get("publish_time") - }) - - # 处理字符串类型的 JSON 返回 (Baidu 常返 JSON 字符串) - if isinstance(results, str) and engine not in ["local", "jina"]: - try: - results = json.loads(results) - except: - pass - - # 转为统一格式 - normalized_results = [] - if isinstance(results, list): - - for i, r in enumerate(results, 1): - title = r.get('title', '') - url = r.get('href') or r.get('url') or r.get('link', '') - content = r.get('body') or r.get('snippet') or r.get('abstract', '') - - if title and url: - normalized_results.append({ - "id": self._generate_hash(url + query, "search_item", i), - "rank": i, - "title": title, - "url": url, - "content": content, - "original_snippet": content, # 保留摘要 - "source": f"Search ({engine})", - "publish_time": datetime.now().isoformat(), # 暂用当前时间 - "crawl_time": datetime.now().isoformat(), - "meta_data": {"query": query, "engine": engine} - }) - - # Fallback if still string and failed to parse - elif isinstance(results, str) and results: - normalized_results.append({"title": query, "url": "", "content": results, "source": engine}) - - # 3. 抓取正文 & 计算情绪 (Enrichment) - # 注意:如果使用 Jina Search,内容已经是 LLM 友好格式,可选择跳过 enrichment - skip_content_enrichment = (engine == "jina") - - if enrich and normalized_results: - logger.info(f"🕸️ Enriching {len(normalized_results)} search results with Jina & Sentiment...") - extractor = ContentExtractor() - - # Lazy load sentiment tool - if not hasattr(self, 'sentiment_tool') or self.sentiment_tool is None: - from .sentiment_tools import SentimentTools - self.sentiment_tool = SentimentTools(self.db) - - for item in normalized_results: - if item.get("url"): - try: - # 如果是 Jina Search,内容已经足够好,跳过额外抓取 - if skip_content_enrichment and item.get("content") and len(item.get("content", "")) > 100: - full_content = item["content"] - else: - # Use Jina Reader to get full content - full_content = extractor.extract_with_jina(item["url"], timeout=60) - - if full_content and len(full_content) > 100: - item["content"] = full_content - - # Calculate sentiment - # Use title + snippet of content for efficiency - text_to_analyze = f"{item['title']} {full_content[:500]}" - sent_result = self.sentiment_tool.analyze_sentiment(text_to_analyze) # Using self.sentiment_tool - score = sent_result.get('score', 0.0) - item["sentiment_score"] = float(score) - - logger.info(f" ✅ Enriched: {item['title'][:20]}... (Sentiment: {score:.2f})") - else: - # Fallback: Use snippet for sentiment - logger.info(f" ⚠️ Content short/failed for {item['url']}, using snippet for sentiment.") - text_to_analyze = f"{item['title']} {item['content']}" # content is snippet here - sent_result = self.sentiment_tool.analyze_sentiment(text_to_analyze) - score = sent_result.get('score', 0.0) - item["sentiment_score"] = float(score) - - except Exception as e: - # Fallback: Use snippet for sentiment on error - logger.warning(f"Failed to enrich {item['url']}: {e}. Using snippet.") - text_to_analyze = f"{item['title']} {item['content']}" - sent_result = self.sentiment_tool.analyze_sentiment(text_to_analyze) - score = sent_result.get('score', 0.0) - item["sentiment_score"] = float(score) - - # 缓存结果 list - if normalized_results: - # Pass list directly, DB manager will handle JSON dump for main cache and populate search_details - # Only cache if NOT from local news reuse (though this logic path is for fresh search) - self.db.save_search_cache(query_hash, query, engine, normalized_results) - - return normalized_results - - except Exception as e: - # 搜索失败时的降级策略 - if engine == "jina": - logger.warning(f"⚠️ Jina search_list failed, falling back to ddg: {query} ({e})") - try: - return self.search_list(query, engine="ddg", max_results=max_results, ttl=ttl, enrich=enrich) - except Exception as e2: - logger.error(f"❌ DDG fallback (search_list) also failed for {query}: {e2}") - elif engine == "ddg": - logger.warning(f"⚠️ DDG search_list failed, falling back to baidu: {query} ({e})") - try: - return self.search_list(query, engine="baidu", max_results=max_results, ttl=ttl, enrich=enrich) - except Exception as e2: - logger.error(f"❌ Baidu fallback (search_list) also failed for {query}: {e2}") - - logger.error(f"❌ Structured search failed for {query}: {e}") - return [] - - def list_similar_queries(self, query: str, limit: int = 5) -> List[Dict]: - """ - 查找与当前查询类似的已缓存查询。 - Agent 可用此方法获取候选缓存,并使用 PROMPTS.md 进行评估以决定是否重用。 - """ - return self.db.find_similar_queries(query, limit=limit) - - - def aggregate_search(self, query: str, engines: Optional[List[str]] = None, max_results: int = 5) -> str: - """ - 使用多个搜索引擎同时搜索并聚合结果,获得更全面的信息覆盖。 - - Args: - query: 搜索关键词。 - engines: 要使用的搜索引擎列表。可选值: ["ddg", "baidu"]。 - 默认同时使用 ddg 和 baidu。 - max_results: 每个引擎期望返回的结果数量。 - - Returns: - 聚合后的搜索结果,按引擎分组显示。 - """ - engines = engines or ["ddg", "baidu"] - aggregated_results = [] - for engine in engines: - res = self.search(query, engine=engine, max_results=max_results) - aggregated_results.append(f"--- Results from {engine.upper()} ---\n{res}") - - return "\n\n".join(aggregated_results) diff --git a/skills/alphaear-search/scripts/sentiment_tools.py b/skills/alphaear-search/scripts/sentiment_tools.py deleted file mode 100644 index f4278b5..0000000 --- a/skills/alphaear-search/scripts/sentiment_tools.py +++ /dev/null @@ -1,287 +0,0 @@ -import os -from typing import Dict, List, Union, Optional -import json -from loguru import logger -from agno.agent import Agent -from .llm.factory import get_model -from .database_manager import DatabaseManager - -# 从环境变量读取默认情绪分析模式 -DEFAULT_SENTIMENT_MODE = os.getenv("SENTIMENT_MODE", "auto") # auto, bert, llm - - -class SentimentTools: - """ - 情绪分析工具 - 支持 LLM 和 BERT 两种模式 - - 模式说明: - - "auto": 自动选择,优先使用 BERT(速度快),不可用时回退到 LLM - - "bert": 强制使用 BERT 模型(需要 transformers 库) - - "llm": 强制使用 LLM(更准确但较慢) - - 可通过环境变量 SENTIMENT_MODE 设置默认模式。 - """ - - def __init__( - self, - db: DatabaseManager, - mode: Optional[str] = None, - model_provider: str = "openai", - model_id: str = "gpt-4o", - ): - """ - 初始化情绪分析工具。 - - Args: - db: 数据库管理器实例 - mode: 分析模式,可选 "auto", "bert", "llm"。None 则使用环境变量默认值。 - model_provider: LLM 提供商,如 "openai", "ust", "deepseek" - model_id: 模型标识符 - """ - self.db = db - self.mode = mode or DEFAULT_SENTIMENT_MODE - self.llm_model = None - self.bert_pipeline = None - - # Initialize LLM - try: - provider = "minimax" if os.getenv("MINIMAX_API_KEY") else model_provider - m_id = ( - os.getenv("LLM_MODEL", "MiniMax-Text-01") - if provider == "minimax" - else model_id - ) - self.llm_model = get_model(provider, m_id) - except Exception as e: - logger.warning(f"LLM initialization skipped: {e}") - - # Initialize BERT if needed - if self.mode in ["bert", "auto"]: - try: - from transformers import ( - pipeline, - AutoTokenizer, - AutoModelForSequenceClassification, - ) - from transformers.utils import logging as transformers_logging - - transformers_logging.set_verbosity_error() # 减少冗余日志 - - bert_model = os.getenv( - "BERT_SENTIMENT_MODEL", - "uer/roberta-base-finetuned-chinanews-chinese", - ) - - # 优先使用本地缓存 - try: - tokenizer = AutoTokenizer.from_pretrained( - bert_model, local_files_only=True - ) - model = AutoModelForSequenceClassification.from_pretrained( - bert_model, local_files_only=True - ) - - self.bert_pipeline = pipeline( - "sentiment-analysis", - model=model, - tokenizer=tokenizer, - device=-1, - ) - logger.info( - f"✅ BERT pipeline loaded from local cache: {bert_model}" - ) - except (OSError, ValueError, ImportError): - # 本地没有,则从网络下载 - logger.info(f"📡 Downloading BERT model: {bert_model}...") - tokenizer = AutoTokenizer.from_pretrained(bert_model) - model = AutoModelForSequenceClassification.from_pretrained( - bert_model - ) - - self.bert_pipeline = pipeline( - "sentiment-analysis", - model=model, - tokenizer=tokenizer, - device=-1, - ) - logger.info( - f"✅ BERT Sentiment pipeline ({bert_model}) initialized." - ) - except ImportError: - logger.warning( - "Transformers library not installed. BERT sentiment analysis disabled." - ) - except Exception as e: - if self.mode == "bert": - logger.error(f"BERT mode requested but failed: {e}") - else: - logger.warning(f"BERT unavailable, using LLM only. Error: {e}") - self.bert_pipeline = None - - def analyze_sentiment(self, text: str) -> Dict[str, Union[float, str]]: - """ - 分析文本的情绪极性。根据初始化时的 mode 自动选择分析方法。 - - Args: - text: 需要分析的文本内容,如新闻标题或摘要。 - - Returns: - 包含以下字段的字典: - - score: 情绪分值,范围 -1.0(极度负面)到 1.0(极度正面),0.0 为中性 - - label: 情绪标签,"positive"/"negative"/"neutral" - - reason: 分析理由(仅 LLM 模式提供详细理由) - """ - if self.mode == "bert" and self.bert_pipeline: - results = self.analyze_sentiment_bert([text]) - return results[0] if results else {"score": 0.0, "label": "error"} - elif self.mode == "llm" or (self.mode == "auto" and not self.bert_pipeline): - return self.analyze_sentiment_llm(text) - else: - # auto mode with BERT available - results = self.analyze_sentiment_bert([text]) - return results[0] if results else {"score": 0.0, "label": "error"} - - def analyze_sentiment_llm(self, text: str) -> Dict[str, Union[float, str]]: - """ - 使用 LLM 进行深度情绪分析,可获得详细的分析理由。 - - Args: - text: 需要分析的文本,最多处理前 1000 字符。 - - Returns: - 包含 score, label, reason 的字典。 - """ - if not self.llm_model: - return {"score": 0.0, "label": "neutral", "error": "LLM not initialized"} - - analyzer = Agent(model=self.llm_model, markdown=True) - prompt = f"""请分析以下金融/新闻文本的情绪极性。 - 返回严格的 JSON 格式: - {{"score": , "label": "", "reason": "<简短理由>"}} - - 文本: {text[:1000]}""" - - try: - response = analyzer.run(prompt) - content = response.content - if "```json" in content: - content = content.split("```json")[1].split("```")[0].strip() - elif "```" in content: - content = content.split("```")[1].split("```")[0].strip() - return json.loads(content) - except Exception as e: - logger.error(f"LLM sentiment failed: {e}") - return {"score": 0.0, "label": "error", "reason": str(e)} - - def analyze_sentiment_bert(self, texts: List[str]) -> List[Dict]: - """ - 使用 BERT 进行批量高速情绪分析。 - - Args: - texts: 需要分析的文本列表。 - - Returns: - 与输入列表等长的分析结果列表。 - """ - if not self.bert_pipeline: - return [ - {"score": 0.0, "label": "error", "reason": "BERT not available"} - ] * len(texts) - - try: - results = self.bert_pipeline(texts, truncation=True, max_length=512) - processed = [] - for r in results: - label = r["label"].lower() - score = r["score"] - - # 标准化不同模型的标签格式 - if "negative" in label or "neg" in label: - score = -score - elif "neutral" in label or "neu" in label: - score = 0.0 - - processed.append( - { - "score": float(round(score, 3)), - "label": "positive" - if score > 0.1 - else ("negative" if score < -0.1 else "neutral"), - "reason": "BERT automated analysis", - } - ) - return processed - except Exception as e: - logger.error(f"BERT analysis failed: {e}") - return [{"score": 0.0, "label": "error", "reason": str(e)}] * len(texts) - - def batch_update_news_sentiment( - self, - source: Optional[str] = None, - limit: int = 50, - use_bert: Optional[bool] = None, - ): - """ - 批量更新数据库中新闻的情绪分数。 - - Args: - source: 筛选特定新闻源,如 "wallstreetcn"。None 则处理所有来源。 - limit: 最多处理的新闻数量。 - use_bert: 是否使用 BERT。None 则根据初始化模式自动决定。 - - Returns: - 成功更新的新闻数量。 - """ - news_items = self.db.get_daily_news(source=source, limit=limit) - to_analyze = [item for item in news_items if not item.get("sentiment_score")] - - if not to_analyze: - return 0 - - # 决定使用哪种方法 - should_use_bert = ( - use_bert - if use_bert is not None - else (self.bert_pipeline is not None and self.mode != "llm") - ) - - updated_count = 0 - cursor = self.db.conn.cursor() - - if should_use_bert and self.bert_pipeline: - logger.info( - f"🚀 Using BERT for batch analysis of {len(to_analyze)} items..." - ) - titles = [item["title"] for item in to_analyze] - results = self.analyze_sentiment_bert(titles) - - for item, analysis in zip(to_analyze, results): - cursor.execute( - """ - UPDATE daily_news - SET sentiment_score = ?, meta_data = json_set(COALESCE(meta_data, '{}'), '$.sentiment_reason', ?) - WHERE id = ? - """, - (analysis["score"], analysis["reason"], item["id"]), - ) - updated_count += 1 - else: - logger.info(f"🚶 Using LLM for analysis of {len(to_analyze)} items...") - for item in to_analyze: - analysis = self.analyze_sentiment_llm(item["title"]) - cursor.execute( - """ - UPDATE daily_news - SET sentiment_score = ?, meta_data = json_set(COALESCE(meta_data, '{}'), '$.sentiment_reason', ?) - WHERE id = ? - """, - ( - analysis.get("score", 0.0), - analysis.get("reason", ""), - item["id"], - ), - ) - updated_count += 1 - - self.db.conn.commit() - return updated_count diff --git a/skills/alphaear-search/tests/test_search.py b/skills/alphaear-search/tests/test_search.py deleted file mode 100644 index 14838b3..0000000 --- a/skills/alphaear-search/tests/test_search.py +++ /dev/null @@ -1,31 +0,0 @@ -import sys -import os -import unittest - -# Add skill root to path -sys.path.append(os.path.abspath(os.path.join(os.path.dirname(__file__), '..'))) - -try: - from scripts.search_tools import SearchTools - from scripts.database_manager import DatabaseManager - from scripts.hybrid_search import InMemoryRAG -except ImportError as e: - print(f"Import Error: {e}") - sys.exit(1) - -class TestSearch(unittest.TestCase): - def test_init(self): - print("Testing SearchTools Iteration...") - db = DatabaseManager(":memory:") - tools = SearchTools(db) - self.assertIsNotNone(tools) - print("SearchTools Initialized.") - - def test_rag(self): - print("Testing InMemoryRAG...") - rag = InMemoryRAG([]) - self.assertIsNotNone(rag) - print("InMemoryRAG Initialized.") - -if __name__ == '__main__': - unittest.main() diff --git a/skills/alphaear-sentiment/SKILL.md b/skills/alphaear-sentiment/SKILL.md deleted file mode 100644 index 2d5fc7f..0000000 --- a/skills/alphaear-sentiment/SKILL.md +++ /dev/null @@ -1,57 +0,0 @@ ---- -name: alphaear-sentiment -description: Analyze finance text sentiment using FinBERT or LLM. Use when the user needs to determine the sentiment (positive/negative/neutral) and score of financial text markets. ---- - -# AlphaEar Sentiment Skill - -## Overview - -This skill provides sentiment analysis capabilities tailored for financial texts, supporting both FinBERT (local model) and LLM-based analysis modes. - -## Capabilities - -## Capabilities - -### 1. Analyze Sentiment (FinBERT / Local) - -Use `scripts/sentiment_tools.py` for high-speed, local sentiment analysis using FinBERT. - -**Key Methods:** - -- `analyze_sentiment(text)`: Get sentiment score and label using localized FinBERT model. - - **Returns**: `{'score': float, 'label': str, 'reason': str}`. - - **Score Range**: -1.0 (Negative) to 1.0 (Positive). -- `batch_update_news_sentiment(source, limit)`: Batch process unanalyzed news in the database (FinBERT only). - -### 2. Analyze Sentiment (LLM / Agentic) - -For higher accuracy or reasoning capabilities, **YOU (the Agent)** should perform the analysis using the Prompt below, calling the LLM directly, and then update the database if necessary. - -#### Sentiment Analysis Prompt - -Use this prompt to analyze financial texts if the local tool is insufficient or if reasoning is required. - -```markdown -请分析以下金融/新闻文本的情绪极性。 -返回严格的 JSON 格式: -{"score": , "label": "", "reason": "<简短理由>"} - -文本: {text} -``` - -**Scoring Guide:** -- **Positive (0.1 to 1.0)**: Optimistic news, profit growth, policy support, etc. -- **Negative (-1.0 to -0.1)**: Losses, sanctions, price drops, pessimism. -- **Neutral (-0.1 to 0.1)**: Factual reporting, sideways movement, ambiguous impact. - -#### Helper Methods -- `update_single_news_sentiment(id, score, reason)`: Use this to save your manual analysis to the database. - -## Dependencies - -- `torch` (for FinBERT) -- `transformers` (for FinBERT) -- `sqlite3` (built-in) - -Ensure `DatabaseManager` is initialized correctly. diff --git a/skills/alphaear-sentiment/scripts/__init__.py b/skills/alphaear-sentiment/scripts/__init__.py deleted file mode 100644 index e69de29..0000000 diff --git a/skills/alphaear-sentiment/scripts/database_manager.py b/skills/alphaear-sentiment/scripts/database_manager.py deleted file mode 100644 index cfc362b..0000000 --- a/skills/alphaear-sentiment/scripts/database_manager.py +++ /dev/null @@ -1,581 +0,0 @@ -import sqlite3 -import json -from datetime import datetime, date -from pathlib import Path -from typing import List, Dict, Optional, Any, Union -import pandas as pd -from loguru import logger - -class DatabaseManager: - """ - AlphaEar 数据库管理器 - 负责存储热点数据、搜索缓存和股价数据 - 使用 SQLite 进行持久化存储 - """ - - def __init__(self, db_path: str = "data/signal_flux.db"): - self.db_path = Path(db_path) - self.db_path.parent.mkdir(parents=True, exist_ok=True) - self.conn = sqlite3.connect(str(self.db_path), check_same_thread=False) - self.conn.row_factory = sqlite3.Row - self._init_db() - logger.info(f"💾 Database initialized at {self.db_path}") - - def _init_db(self): - """初始化表结构""" - cursor = self.conn.cursor() - - # 1. 每日热点新闻表 - cursor.execute(""" - CREATE TABLE IF NOT EXISTS daily_news ( - id TEXT PRIMARY KEY, - source TEXT, - rank INTEGER, - title TEXT, - url TEXT, - content TEXT, - publish_time TEXT, - crawl_time TEXT, - sentiment_score REAL, - analysis TEXT, - meta_data TEXT - ) - """) - - # 尝试添加 analysis 列(如果表已存在但没有该列) - try: - cursor.execute("ALTER TABLE daily_news ADD COLUMN analysis TEXT") - except: - pass # 列已存在 - - - # 2. 搜索缓存表 (原有 JSON 缓存) - cursor.execute(""" - CREATE TABLE IF NOT EXISTS search_cache ( - query_hash TEXT PRIMARY KEY, - query TEXT, - engine TEXT, - results TEXT, - timestamp TEXT - ) - """) - - # 2.5 搜索详情表 (展开的搜索结果) - cursor.execute(""" - CREATE TABLE IF NOT EXISTS search_detail ( - id TEXT, - query_hash TEXT, - rank INTEGER, - title TEXT, - url TEXT, - content TEXT, - publish_time TEXT, - crawl_time TEXT, - sentiment_score REAL, - source TEXT, - meta_data TEXT, - PRIMARY KEY (query_hash, id) - ) - """) - - # 3. 股价数据表 - cursor.execute(""" - CREATE TABLE IF NOT EXISTS stock_prices ( - ticker TEXT, - date TEXT, - open REAL, - close REAL, - high REAL, - low REAL, - volume REAL, - change_pct REAL, - PRIMARY KEY (ticker, date) - ) - """) - - # 4. 股票列表表 (用于检索) - cursor.execute(""" - CREATE TABLE IF NOT EXISTS stock_list ( - code TEXT PRIMARY KEY, - name TEXT - ) - """) - - # 5. 投资信号表 (ISQ Framework) - cursor.execute(""" - CREATE TABLE IF NOT EXISTS signals ( - signal_id TEXT PRIMARY KEY, - title TEXT, - summary TEXT, - transmission_chain TEXT, - sentiment_score REAL, - confidence REAL, - intensity INTEGER, - expected_horizon TEXT, - price_in_status TEXT, - impact_tickers TEXT, - industry_tags TEXT, - sources TEXT, - user_id TEXT, - created_at TEXT - ) - """) - - - - # 6. 创建索引以优化查询性能 - cursor.execute("CREATE INDEX IF NOT EXISTS idx_news_crawl_time ON daily_news(crawl_time)") - cursor.execute("CREATE INDEX IF NOT EXISTS idx_news_source ON daily_news(source)") - cursor.execute("CREATE INDEX IF NOT EXISTS idx_search_cache_timestamp ON search_cache(timestamp)") - cursor.execute("CREATE INDEX IF NOT EXISTS idx_stock_prices_ticker_date ON stock_prices(ticker, date)") - # 尝试添加 user_id 列到 signals 表 - try: - cursor.execute("ALTER TABLE signals ADD COLUMN user_id TEXT") - except: - pass - - cursor.execute("CREATE INDEX IF NOT EXISTS idx_signals_user_id ON signals(user_id)") - - self.conn.commit() - - # - # self.conn.commit() - - - # --- 新闻数据操作 --- - - def save_daily_news(self, news_list: List[Dict]) -> int: - """保存热点新闻,包含发布时间与抓取时间""" - cursor = self.conn.cursor() - count = 0 - crawl_time = datetime.now().isoformat() - - for news in news_list: - try: - # 兼容不同来源的 ID 生成逻辑 - news_id = news.get('id') or f"{news.get('source')}_{news.get('rank')}_{crawl_time[:10]}" - cursor.execute(""" - INSERT OR REPLACE INTO daily_news - (id, source, rank, title, url, content, publish_time, crawl_time, sentiment_score, meta_data) - VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?) - """, ( - news_id, - news.get('source'), - news.get('rank'), - news.get('title'), - news.get('url'), - news.get('content', ''), - news.get('publish_time'), # 新增支持发布时间 - crawl_time, - news.get('sentiment_score'), - json.dumps(news.get('meta_data', {})) - )) - count += 1 - except sqlite3.Error as e: - logger.error(f"Database error saving news item {news.get('title')}: {e}") - except Exception as e: - logger.error(f"Unexpected error saving news item {news.get('title')}: {e}") - - self.conn.commit() - return count - - def get_daily_news(self, source: Optional[str] = None, limit: int = 100, days: int = 1) -> List[Dict]: - """获取最近 N 天的热点新闻""" - cursor = self.conn.cursor() - # 使用 crawl_time 过滤,保证结果的新鲜度 - time_threshold = (datetime.now().timestamp() - days * 86400) - time_threshold_str = datetime.fromtimestamp(time_threshold).isoformat() - - query = "SELECT * FROM daily_news WHERE crawl_time >= ?" - params = [time_threshold_str] - - if source: - query += " AND source = ?" - params.append(source) - - query += " ORDER BY crawl_time DESC, rank LIMIT ?" - params.append(limit) - - cursor.execute(query, params) - return [dict(row) for row in cursor.fetchall()] - - def lookup_reference_by_url(self, url: str) -> Optional[Dict[str, Any]]: - """Best-effort lookup of a source item by URL. - - This is used to render a stable bibliography from DB-backed metadata. - It searches both `daily_news` and `search_detail`. - """ - url = (url or "").strip() - if not url: - return None - - cursor = self.conn.cursor() - - try: - cursor.execute( - """ - SELECT title, source, publish_time, crawl_time, url - FROM daily_news - WHERE url = ? - ORDER BY crawl_time DESC - LIMIT 1 - """, - (url,), - ) - row = cursor.fetchone() - if row: - return dict(row) - except Exception: - pass - - try: - cursor.execute( - """ - SELECT title, source, publish_time, crawl_time, url - FROM search_detail - WHERE url = ? - ORDER BY crawl_time DESC - LIMIT 1 - """, - (url,), - ) - row = cursor.fetchone() - if row: - return dict(row) - except Exception: - pass - - return None - - def delete_news(self, news_id: str) -> bool: - """删除特定新闻""" - cursor = self.conn.cursor() - cursor.execute("DELETE FROM daily_news WHERE id = ?", (news_id,)) - self.conn.commit() - return cursor.rowcount > 0 - - def update_news_content(self, news_id: str, content: str = None, analysis: str = None) -> bool: - """更新新闻的内容或分析结果""" - cursor = self.conn.cursor() - updates = [] - params = [] - - if content is not None: - updates.append("content = ?") - params.append(content) - if analysis is not None: - updates.append("analysis = ?") - params.append(analysis) - - if not updates: - return False - - params.append(news_id) - query = f"UPDATE daily_news SET {', '.join(updates)} WHERE id = ?" - cursor.execute(query, params) - self.conn.commit() - return cursor.rowcount > 0 - - # --- 搜索缓存辅助 --- - - def get_search_cache(self, query_hash: str, ttl_seconds: Optional[int] = None) -> Optional[Dict]: - """获取搜索缓存 (优先查 search_detail)""" - cursor = self.conn.cursor() - - # 1. 尝试从 search_detail 获取展开的结构化数据 - cursor.execute(""" - SELECT * FROM search_detail - WHERE query_hash = ? - ORDER BY rank - """, (query_hash,)) - details = [dict(row) for row in cursor.fetchall()] - - if details: - # 检查 TTL (取第一条的时间) - first_time = datetime.fromisoformat(details[0]['crawl_time']) - if ttl_seconds and (datetime.now() - first_time).total_seconds() > ttl_seconds: - logger.info(f"⌛ Detailed cache expired for hash {query_hash}") - pass # Expired, fall through or return None? If Detail expired, Cache likely expired too. - # But let's check basic cache just in case metadata differs? - # Actually if details exist, we prefer them. If expired, we return None. - return None - - logger.info(f"✅ Hit detailed search cache for {query_hash} ({len(details)} items)") - # Reconstruct the expected 'results' list format for SearchTools - # SearchTools expects a list of dicts. - # We return a dict wrapper to match get_search_cache signature returning Dict usually containing 'results' string. - # But SearchTools logic: - # cache = db.get_search_cache(...) - # cached_data = json.loads(cache['results']) - - # To minimize SearchTools changes, we can return a dict mimicking the old structure - # OR Change SearchTools to handle list return. - # Let's return a special dict that SearchTools can recognize or just format it as before. - return {"results": json.dumps(details), "timestamp": details[0]['crawl_time']} - - # 2. Fallback to old table - cursor.execute("SELECT * FROM search_cache WHERE query_hash = ?", (query_hash,)) - row = cursor.fetchone() - - if not row: - return None - - row_dict = dict(row) - if ttl_seconds: - cache_time = datetime.fromisoformat(row_dict['timestamp']) - if (datetime.now() - cache_time).total_seconds() > ttl_seconds: - logger.info(f"⌛ Cache expired for hash {query_hash}") - return None - - return row_dict - - def save_search_cache(self, query_hash: str, query: str, engine: str, results: Union[str, List[Dict]]): - """保存搜索结果 (同时保存到 search_cache 和 search_detail)""" - cursor = self.conn.cursor() - current_time = datetime.now().isoformat() - - results_str = results if isinstance(results, str) else json.dumps(results) - - # 1. Save summary to search_cache - cursor.execute(""" - INSERT OR REPLACE INTO search_cache (query_hash, query, engine, results, timestamp) - VALUES (?, ?, ?, ?, ?) - """, (query_hash, query, engine, results_str, current_time)) - - # 2. Save details to search_detail if results is a list - if isinstance(results, list): - for item in results: - try: - item_id = item.get('id') or f"{hash(item.get('url', ''))}" - cursor.execute(""" - INSERT OR REPLACE INTO search_detail - (id, query_hash, rank, title, url, content, publish_time, crawl_time, sentiment_score, source, meta_data) - VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?) - """, ( - str(item_id), - query_hash, - item.get('rank', 0), - item.get('title'), - item.get('url'), - item.get('content', ''), - item.get('publish_time'), - item.get('crawl_time') or current_time, - item.get('sentiment_score'), - item.get('source'), - json.dumps(item.get('meta_data', {})) - )) - except sqlite3.Error as e: - logger.error(f"Database error saving search detail {item.get('title')}: {e}") - except Exception as e: - logger.error(f"Unexpected error saving search detail {item.get('title')}: {e}") - - self.conn.commit() - - def find_similar_queries(self, query: str, limit: int = 5) -> List[Dict]: - """模糊搜索相似的已缓存查询""" - cursor = self.conn.cursor() - - # Simple fuzzy match: query in cached OR cached in query - q_wild = f"%{query}%" - cursor.execute(""" - SELECT query, query_hash, timestamp, results - FROM search_cache - WHERE query LIKE ? OR ? LIKE ('%' || query || '%') - ORDER BY timestamp DESC - LIMIT ? - """, (q_wild, query, limit)) - - return [dict(row) for row in cursor.fetchall()] - - def search_local_news(self, query: str, limit: int = 5) -> List[Dict]: - """从本地 daily_news 搜索相关新闻""" - cursor = self.conn.cursor() - q_wild = f"%{query}%" - # Search title and content - cursor.execute(""" - SELECT * FROM daily_news - WHERE title LIKE ? OR content LIKE ? - ORDER BY crawl_time DESC - LIMIT ? - """, (q_wild, q_wild, limit)) - return [dict(row) for row in cursor.fetchall()] - - # --- 股票数据操作 --- - - def save_stock_list(self, df: pd.DataFrame): - """保存股票列表到 stock_list 表""" - cursor = self.conn.cursor() - try: - # 清空旧表 - cursor.execute("DELETE FROM stock_list") - - # 批量插入 - data = df[['code', 'name']].to_dict('records') - cursor.executemany( - "INSERT INTO stock_list (code, name) VALUES (:code, :name)", - data - ) - self.conn.commit() - except sqlite3.Error as e: - logger.error(f"Database error saving stock list: {e}") - except Exception as e: - logger.error(f"Unexpected error saving stock list: {e}") - - def search_stock(self, query: str, limit: int = 5) -> List[Dict]: - """模糊搜索股票代码或名称""" - cursor = self.conn.cursor() - wild = f"%{query}%" - cursor.execute(""" - SELECT code, name FROM stock_list - WHERE code LIKE ? OR name LIKE ? - LIMIT ? - """, (wild, wild, limit)) - return [dict(row) for row in cursor.fetchall()] - - def get_stock_by_code(self, code: str) -> Optional[Dict[str, str]]: - """精确按代码获取股票信息。 - - Args: - code: 股票代码(A股6位 / 港股5位),必须为纯数字字符串。 - - Returns: - dict: {"code": str, "name": str} 或 None。 - """ - if not code: - return None - clean = "".join([c for c in str(code).strip() if c.isdigit()]) - if not clean: - return None - - cursor = self.conn.cursor() - cursor.execute("SELECT code, name FROM stock_list WHERE code = ? LIMIT 1", (clean,)) - row = cursor.fetchone() - return dict(row) if row else None - - def save_stock_prices(self, ticker: str, df: pd.DataFrame): - """保存股价历史数据""" - if df.empty: - return - - cursor = self.conn.cursor() - - # 确保 DataFrame 有必要的列 - required_cols = ['date', 'open', 'close', 'high', 'low', 'volume', 'change_pct'] - for col in required_cols: - if col not in df.columns: - logger.warning(f"Missing column {col} in stock data for {ticker}") - return - - try: - for _, row in df.iterrows(): - cursor.execute(""" - INSERT OR REPLACE INTO stock_prices - (ticker, date, open, close, high, low, volume, change_pct) - VALUES (?, ?, ?, ?, ?, ?, ?, ?) - """, ( - ticker, - row['date'], - row['open'], - row['close'], - row['high'], - row['low'], - row['volume'], - row['change_pct'] - )) - self.conn.commit() - except sqlite3.Error as e: - logger.error(f"Database error saving stock prices for {ticker}: {e}") - except Exception as e: - logger.error(f"Unexpected error saving stock prices for {ticker}: {e}") - - def get_stock_prices(self, ticker: str, start_date: str, end_date: str) -> pd.DataFrame: - """获取指定日期范围的股价数据""" - cursor = self.conn.cursor() - - cursor.execute(""" - SELECT * FROM stock_prices - WHERE ticker = ? AND date >= ? AND date <= ? - ORDER BY date - """, (ticker, start_date, end_date)) - - rows = cursor.fetchall() - if not rows: - return pd.DataFrame() - - columns = ['ticker', 'date', 'open', 'close', 'high', 'low', 'volume', 'change_pct'] - return pd.DataFrame([dict(row) for row in rows], columns=columns) - - def execute_query(self, query: str, params: tuple = ()) -> List[Any]: - """执行自定义 SQL 查询""" - try: - cursor = self.conn.cursor() - cursor.execute(query, params) - if query.strip().upper().startswith("SELECT"): - return cursor.fetchall() - else: - self.conn.commit() - return [] - except sqlite3.Error as e: - logger.error(f"SQL execution failed (Database error): {e}") - return [] - except Exception as e: - logger.error(f"SQL execution failed (Unexpected error): {e}") - return [] - - # --- 投资信号操作 (ISQ Framework) --- - - def save_signal(self, signal: Dict[str, Any]): - """保存投资信号""" - cursor = self.conn.cursor() - created_at = datetime.now().isoformat() - - cursor.execute(""" - INSERT OR REPLACE INTO signals - (signal_id, title, summary, transmission_chain, sentiment_score, - confidence, intensity, expected_horizon, price_in_status, - impact_tickers, industry_tags, sources, user_id, created_at) - VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?) - """, ( - signal.get('signal_id'), - signal.get('title'), - signal.get('summary'), - json.dumps(signal.get('transmission_chain', [])), - signal.get('sentiment_score', 0.0), - signal.get('confidence', 0.0), - signal.get('intensity', 1), - signal.get('expected_horizon', 'T+0'), - signal.get('price_in_status', '未知'), - json.dumps(signal.get('impact_tickers', [])), - json.dumps(signal.get('industry_tags', [])), - json.dumps(signal.get('sources', [])), - signal.get('user_id'), - created_at - )) - self.conn.commit() - - def get_recent_signals(self, limit: int = 20, user_id: Optional[str] = None) -> List[Dict]: - """获取最近的投资信号""" - cursor = self.conn.cursor() - if user_id: - cursor.execute("SELECT * FROM signals WHERE user_id = ? ORDER BY created_at DESC LIMIT ?", (user_id, limit)) - else: - cursor.execute("SELECT * FROM signals ORDER BY created_at DESC LIMIT ?", (limit,)) - rows = cursor.fetchall() - - signals = [] - for row in rows: - d = dict(row) - # 解析 JSON 字段 - for field in ['transmission_chain', 'impact_tickers', 'industry_tags', 'sources']: - if d.get(field): - try: - d[field] = json.loads(d[field]) - except: - pass - signals.append(d) - return signals - - def close(self): - if self.conn: - self.conn.close() - logger.info("Database connection closed.") - diff --git a/skills/alphaear-sentiment/scripts/llm/capability.py b/skills/alphaear-sentiment/scripts/llm/capability.py deleted file mode 100644 index de9de32..0000000 --- a/skills/alphaear-sentiment/scripts/llm/capability.py +++ /dev/null @@ -1,85 +0,0 @@ -import os -from typing import Optional, List, Dict, Any -from agno.agent import Agent -from agno.models.base import Model -from loguru import logger -from .llm.factory import get_model - - -def test_tool_call_support(model: Model) -> bool: - """ - 测试模型是否支持原生的 Tool Call (Function Calling)。 - 通过尝试执行一个简单的加法工具来验证。 - """ - - def get_current_weather(location: str): - """获取指定地点的天气""" - return f"{location} 的天气是晴天,25度。" - - test_agent = Agent( - model=model, - tools=[get_current_weather], - instructions="请调用工具查询北京的天气,并直接返回工具的输出结果。", - ) - - try: - # 运行一个简单的任务,观察是否触发了 tool_call - response = test_agent.run("北京天气怎么样?") - - # 检查 response 中是否包含 tool_calls - # Agno 的 RunResponse 对象通常包含 messages,我们可以检查最后几条消息 - has_tool_call = False - for msg in response.messages: - if hasattr(msg, "tool_calls") and msg.tool_calls: - has_tool_call = True - break - - if has_tool_call: - logger.info(f"✅ Model {model.id} supports native tool calling.") - return True - else: - # 如果没有 tool_calls 但返回了正确答案,可能是模型通过纯文本模拟了工具调用(ReAct) - # 或者根本没用工具。对于原生支持的判断,我们坚持要求有 tool_calls 结构。 - logger.warning( - f"⚠️ Model {model.id} did NOT use native tool calling structure." - ) - return False - - except Exception as e: - logger.error(f"❌ Error testing tool call for {model.id}: {e}") - return False - - -class ModelCapabilityRegistry: - """ - 模型能力注册表,用于缓存和管理不同模型的能力测试结果。 - """ - - _cache = {} - - @classmethod - def get_capabilities( - cls, provider: str, model_id: str, **kwargs - ) -> Dict[str, bool]: - key = f"{provider}:{model_id}" - if key not in cls._cache: - logger.info(f"🔍 Testing capabilities for {key}...") - model = get_model(provider, model_id, **kwargs) - supports_tool_call = test_tool_call_support(model) - cls._cache[key] = {"supports_tool_call": supports_tool_call} - return cls._cache[key] - - -if __name__ == "__main__": - import os - from skills._env_loader import load_unified_env - - load_unified_env() - - # 测试当前配置的模型 - p = os.getenv("LLM_PROVIDER", "minimax") - m = os.getenv("LLM_MODEL", "Qwen") - - print(f"Testing {p}/{m}...") - res = ModelCapabilityRegistry.get_capabilities(p, m) - print(f"Result: {res}") diff --git a/skills/alphaear-sentiment/scripts/llm/factory.py b/skills/alphaear-sentiment/scripts/llm/factory.py deleted file mode 100644 index 09b6ea5..0000000 --- a/skills/alphaear-sentiment/scripts/llm/factory.py +++ /dev/null @@ -1,114 +0,0 @@ -import os -from agno.models.openai import OpenAIChat -from agno.models.ollama import Ollama -from agno.models.dashscope import DashScope -from agno.models.deepseek import DeepSeek -from agno.models.openrouter import OpenRouter - -def get_model(model_provider: str, model_id: str, **kwargs): - """ - Factory to get the appropriate LLM model. - - Args: - model_provider: "openai", "ollama", "deepseek" - model_id: The specific model ID (e.g., "gpt-4o", "llama3", "deepseek-chat") - **kwargs: Additional arguments for the model constructor - """ - if model_provider == "openai": - return OpenAIChat(id=model_id, **kwargs) - - elif model_provider == "ollama": - return Ollama(id=model_id, **kwargs) - - elif model_provider == "deepseek": - # DeepSeek is OpenAI compatible - api_key = os.getenv("DEEPSEEK_API_KEY") - if not api_key: - print("Warning: DEEPSEEK_API_KEY not set.") - - return DeepSeek( - id=model_id, - api_key=api_key, - **kwargs - ) - elif model_provider == "dashscope": - api_key = os.getenv("DASHSCOPE_API_KEY") - if not api_key: - print("Warning: DASHSCOPE_API_KEY not set.") - - return DashScope( - id=model_id, - base_url="https://dashscope.aliyuncs.com/compatible-mode/v1", - api_key=api_key, - **kwargs - ) - elif model_provider == 'openrouter': - api_key = os.getenv("OPENROUTER_API_KEY") - if not api_key: - print('Warning: OPENROUTER_API_KEY not set.') - - return OpenRouter( - id=model_id, - api_key=api_key, - **kwargs - ) - - elif model_provider == 'zai': - api_key = os.getenv("ZAI_KEY_API") - if not api_key: - print('Warning: ZAI_KEY_API not set.') - - # role_map to ensure compatibility. - default_role_map = { - "system": "system", - "user": "user", - "assistant": "assistant", - "tool": "tool", - "model": "assistant", - } - - # Allow callers to override role_map via kwargs, otherwise use default - role_map = kwargs.pop("role_map", default_role_map) - - return OpenAIChat( - id=model_id, - base_url="https://api.z.ai/api/paas/v4", - api_key=api_key, - timeout=60, - role_map=role_map, - extra_body={"enable_thinking": False}, # TODO: one more setting for thinking - **kwargs - ) - - elif model_provider == 'ust': - api_key = os.getenv("UST_KEY_API") - if not api_key: - print('Warning: UST_KEY_API not set.') - - # Some UST-compatible endpoints expect the standard OpenAI role names - # (e.g. "system", "user", "assistant") rather than Agno's default - # mapping which maps "system" -> "developer". Provide an explicit - # role_map to ensure compatibility. - default_role_map = { - "system": "system", - "user": "user", - "assistant": "assistant", - "tool": "tool", - "model": "assistant", - } - - # Allow callers to override role_map via kwargs, otherwise use default - role_map = kwargs.pop("role_map", default_role_map) - - return OpenAIChat( - id=model_id, - api_key=api_key, - base_url=os.getenv("UST_URL"), - role_map=role_map, - extra_body={"enable_thinking": False}, # TODO: one more setting for thinking - **kwargs - ) - - else: - raise ValueError(f"Unknown model provider: {model_provider}") - diff --git a/skills/alphaear-sentiment/scripts/llm/router.py b/skills/alphaear-sentiment/scripts/llm/router.py deleted file mode 100644 index 3a3cede..0000000 --- a/skills/alphaear-sentiment/scripts/llm/router.py +++ /dev/null @@ -1,80 +0,0 @@ -import os -from typing import Optional, List, Dict, Any, Union -from agno.models.base import Model -from loguru import logger -from .llm.factory import get_model -from utils.llm.capability import ModelCapabilityRegistry -from skills._env_loader import load_unified_env - -load_unified_env() - - -class ModelRouter: - """ - 模型路由管理器 - - 功能: - 1. 管理“推理/写作模型” (Reasoning Model) 和“工具调用模型” (Tool Model)。 - 2. 根据任务需求自动选择合适的模型。 - """ - - def __init__(self): - # 默认从环境变量读取 - self.reasoning_provider = os.getenv( - "REASONING_MODEL_PROVIDER", os.getenv("LLM_PROVIDER", "openai") - ) - self.reasoning_id = os.getenv( - "REASONING_MODEL_ID", os.getenv("LLM_MODEL", "gpt-4o") - ) - self.reasoning_host = os.getenv("REASONING_MODEL_HOST", os.getenv("LLM_HOST")) - - self.tool_provider = os.getenv("TOOL_MODEL_PROVIDER", self.reasoning_provider) - self.tool_id = os.getenv("TOOL_MODEL_ID", self.reasoning_id) - self.tool_host = os.getenv("TOOL_MODEL_HOST", self.reasoning_host) - - self._reasoning_model = None - self._tool_model = None - - logger.info( - f"🤖 ModelRouter initialized: Reasoning={self.reasoning_id} ({self.reasoning_host or 'default'}), Tool={self.tool_id} ({self.tool_host or 'default'})" - ) - - def get_reasoning_model(self, **kwargs) -> Model: - if not self._reasoning_model: - # 优先使用路由配置的 host - if self.reasoning_host and "host" not in kwargs: - kwargs["host"] = self.reasoning_host - self._reasoning_model = get_model( - self.reasoning_provider, self.reasoning_id, **kwargs - ) - return self._reasoning_model - - def get_tool_model(self, **kwargs) -> Model: - if not self._tool_model: - # 优先使用路由配置的 host - if self.tool_host and "host" not in kwargs: - kwargs["host"] = self.tool_host - - # 检查 tool_model 是否真的支持 tool call - caps = ModelCapabilityRegistry.get_capabilities( - self.tool_provider, self.tool_id, **kwargs - ) - if not caps["supports_tool_call"]: - logger.warning( - f"⚠️ Configured tool model {self.tool_id} might not support native tool calls! Consider using ReAct mode or a different model." - ) - - self._tool_model = get_model(self.tool_provider, self.tool_id, **kwargs) - return self._tool_model - - def get_model_for_agent(self, has_tools: bool = False, **kwargs) -> Model: - """ - 根据 Agent 是否包含工具来返回合适的模型。 - """ - if has_tools: - return self.get_tool_model(**kwargs) - return self.get_reasoning_model(**kwargs) - - -# 全局单例 -router = ModelRouter() diff --git a/skills/alphaear-sentiment/scripts/sentiment_tools.py b/skills/alphaear-sentiment/scripts/sentiment_tools.py deleted file mode 100644 index 330a47e..0000000 --- a/skills/alphaear-sentiment/scripts/sentiment_tools.py +++ /dev/null @@ -1,205 +0,0 @@ -import os -from typing import Dict, List, Union, Optional -import json -from loguru import logger -# IMPORTS REMOVED: agno.agent, get_model -# Internal LLM logic has been removed to delegate analysis to the calling Agent. -from .database_manager import DatabaseManager - -# 从环境变量读取默认情绪分析模式 -DEFAULT_SENTIMENT_MODE = os.getenv("SENTIMENT_MODE", "auto") # auto, bert, llm - -class SentimentTools: - """ - 情绪分析工具 - 支持 LLM 和 BERT 两种模式 - - 模式说明: - - "auto": 自动选择,优先使用 BERT(速度快),不可用时回退到 LLM - - "bert": 强制使用 BERT 模型(需要 transformers 库) - - "llm": 强制使用 LLM(更准确但较慢) - - 可通过环境变量 SENTIMENT_MODE 设置默认模式。 - """ - - def __init__(self, db: DatabaseManager, mode: Optional[str] = None): - """ - 初始化情绪分析工具。 - - Args: - db: 数据库管理器实例 - mode: 分析模式,可选 "auto", "bert", "llm"。None 则使用环境变量默认值。 - model_provider: LLM 提供商,如 "openai", "ust", "deepseek" - model_id: 模型标识符 - """ - self.db = db - self.mode = mode or DEFAULT_SENTIMENT_MODE - self.bert_pipeline = None - - # LLM initialization removed. Agent should perform analysis if needed. - - # Initialize BERT if needed - if self.mode in ["bert", "auto"]: - try: - from transformers import pipeline, AutoTokenizer, AutoModelForSequenceClassification - from transformers.utils import logging as transformers_logging - transformers_logging.set_verbosity_error() # 减少冗余日志 - - bert_model = os.getenv("BERT_SENTIMENT_MODEL", "uer/roberta-base-finetuned-chinanews-chinese") - - # 优先使用本地缓存 - try: - tokenizer = AutoTokenizer.from_pretrained(bert_model, local_files_only=True) - model = AutoModelForSequenceClassification.from_pretrained(bert_model, local_files_only=True) - - self.bert_pipeline = pipeline( - "sentiment-analysis", - model=model, - tokenizer=tokenizer, - device=-1 - ) - logger.info(f"✅ BERT pipeline loaded from local cache: {bert_model}") - except (OSError, ValueError, ImportError): - # 本地没有,则从网络下载 - logger.info(f"📡 Downloading BERT model: {bert_model}...") - tokenizer = AutoTokenizer.from_pretrained(bert_model) - model = AutoModelForSequenceClassification.from_pretrained(bert_model) - - self.bert_pipeline = pipeline( - "sentiment-analysis", - model=model, - tokenizer=tokenizer, - device=-1 - ) - logger.info(f"✅ BERT Sentiment pipeline ({bert_model}) initialized.") - except ImportError: - logger.warning("Transformers library not installed. BERT sentiment analysis disabled.") - except Exception as e: - if self.mode == "bert": - logger.error(f"BERT mode requested but failed: {e}") - else: - logger.warning(f"BERT unavailable, using LLM only. Error: {e}") - self.bert_pipeline = None - - - def analyze_sentiment(self, text: str) -> Dict[str, Union[float, str]]: - """ - 分析文本的情绪极性。仅支持 BERT 模式。 - 如需 LLM 分析,请 Agent 按照 SKILL.md 中的 Prompt 自行执行。 - - Args: - text: 需要分析的文本内容。 - - Returns: - BERT 分析结果,或错误信息。 - """ - if self.bert_pipeline: - results = self.analyze_sentiment_bert([text]) - return results[0] if results else {"score": 0.0, "label": "error"} - else: - return { - "score": 0.0, - "label": "error", - "reason": "BERT pipeline not initialized. For LLM analysis, please manually execute the prompt in SKILL.md." - } - - def update_single_news_sentiment(self, news_id: Union[str, int], score: float, reason: str = "") -> bool: - """ - 允许 Agent 将手动分析的结果保存到数据库。 - - Args: - news_id: 新闻 ID - score: -1.0 到 1.0 - reason: 分析理由 - - Returns: - Success bool - """ - try: - cursor = self.db.conn.cursor() - cursor.execute(""" - UPDATE daily_news - SET sentiment_score = ?, meta_data = json_set(COALESCE(meta_data, '{}'), '$.sentiment_reason', ?) - WHERE id = ? - """, (score, reason, news_id)) - self.db.conn.commit() - return True - except Exception as e: - logger.error(f"Failed to update sentiment for {news_id}: {e}") - return False - - def analyze_sentiment_bert(self, texts: List[str]) -> List[Dict]: - """ - 使用 BERT 进行批量高速情绪分析。 - - Args: - texts: 需要分析的文本列表。 - - Returns: - 与输入列表等长的分析结果列表。 - """ - if not self.bert_pipeline: - return [{"score": 0.0, "label": "error", "reason": "BERT not available"}] * len(texts) - - try: - results = self.bert_pipeline(texts, truncation=True, max_length=512) - processed = [] - for r in results: - label = r['label'].lower() - score = r['score'] - - # 标准化不同模型的标签格式 - if 'negative' in label or 'neg' in label: - score = -score - elif 'neutral' in label or 'neu' in label: - score = 0.0 - - processed.append({ - "score": float(round(score, 3)), - "label": "positive" if score > 0.1 else ("negative" if score < -0.1 else "neutral"), - "reason": "BERT automated analysis" - }) - return processed - except Exception as e: - logger.error(f"BERT analysis failed: {e}") - return [{"score": 0.0, "label": "error", "reason": str(e)}] * len(texts) - - def batch_update_news_sentiment(self, source: Optional[str] = None, limit: int = 50, use_bert: Optional[bool] = None): - """ - 批量更新数据库中新闻的情绪分数。 - - Args: - source: 筛选特定新闻源,如 "wallstreetcn"。None 则处理所有来源。 - limit: 最多处理的新闻数量。 - use_bert: 是否使用 BERT。None 则根据初始化模式自动决定。 - - Returns: - 成功更新的新闻数量。 - """ - news_items = self.db.get_daily_news(source=source, limit=limit) - to_analyze = [item for item in news_items if not item.get('sentiment_score')] - - if not to_analyze: - return 0 - - updated_count = 0 - cursor = self.db.conn.cursor() - - # 决定使用哪种方法 - if self.bert_pipeline: - logger.info(f"🚀 Using BERT for batch analysis of {len(to_analyze)} items...") - titles = [item['title'] for item in to_analyze] - results = self.analyze_sentiment_bert(titles) - - for item, analysis in zip(to_analyze, results): - cursor.execute(""" - UPDATE daily_news - SET sentiment_score = ?, meta_data = json_set(COALESCE(meta_data, '{}'), '$.sentiment_reason', ?) - WHERE id = ? - """, (analysis['score'], analysis['reason'], item['id'])) - updated_count += 1 - else: - logger.warning("BERT pipeline not available. Batch update skipped. Please use Agentic analysis for high-quality results.") - - self.db.conn.commit() - return updated_count - diff --git a/skills/alphaear-sentiment/tests/test_sentiment.py b/skills/alphaear-sentiment/tests/test_sentiment.py deleted file mode 100644 index 3e0549c..0000000 --- a/skills/alphaear-sentiment/tests/test_sentiment.py +++ /dev/null @@ -1,25 +0,0 @@ -import sys -import os -import unittest - -# Add skill root to path -sys.path.append(os.path.abspath(os.path.join(os.path.dirname(__file__), '..'))) - -try: - from scripts.sentiment_tools import SentimentTools - from scripts.database_manager import DatabaseManager -except ImportError as e: - print(f"Import Error: {e}") - sys.exit(1) - -class TestSentiment(unittest.TestCase): - def test_init(self): - print("Testing SentimentTools Iteration...") - db = DatabaseManager(":memory:") - # Mock mode="llm" to avoid loading large models or needing keys - tools = SentimentTools(db, mode="llm") - self.assertIsNotNone(tools) - print("SentimentTools Initialized.") - -if __name__ == '__main__': - unittest.main() diff --git a/skills/alphaear-signal-tracker/SKILL.md b/skills/alphaear-signal-tracker/SKILL.md deleted file mode 100644 index f4f4a28..0000000 --- a/skills/alphaear-signal-tracker/SKILL.md +++ /dev/null @@ -1,51 +0,0 @@ ---- -name: alphaear-signal-tracker -description: Track finance investment signal evolution and update logic based on new finance market information. Use when monitoring finance signals and determining if they are strengthened, weakened, or falsified. ---- - -# AlphaEar Signal Tracker Skill - -## Overview - -This skill provides logic to track and update investment signals. It assesses how new market information impacts existing signals (Strengthened, Weakened, Falsified, or Unchanged). - -## Capabilities - -### 1. Track Signal Evolution - -### 1. Track Signal Evolution (Agentic Workflow) - -**YOU (the Agent)** are the Tracker. Use the prompts in `references/PROMPTS.md`. - -**Workflow:** -1. **Research**: Use **FinResearcher Prompt** to gather facts/price for a signal. -2. **Analyze**: Use **FinAnalyst Prompt** to generate the initial `InvestmentSignal`. -3. **Track**: For existing signals, use **Signal Tracking Prompt** to assess evolution (Strengthened/Weakened/Falsified) based on new info. - -**Tools:** -- Use `alphaear-search` and `alphaear-stock` skills to gather the necessary data. -- Use `scripts/fin_agent.py` helper `_sanitize_signal_output` if needing to clean JSON. - -**Key Logic:** - -- **Input**: Existing Signal State + New Information (News/Price). -- **Process**: - 1. Compare new info with signal thesis. - 2. Determine impact direction (Positive/Negative/Neutral). - 3. Update confidence and intensity. -- **Output**: Updated Signal. - -**Example Usage (Conceptual):** - -```python -# This skill is currently a pattern extracted from FinAgent. -# In a future refactor, it should be a standalone utility class. -# For now, refer to `scripts/fin_agent.py`'s `track_signal` method implementation. -``` - -## Dependencies - -- `agno` (Agent framework) -- `sqlite3` (built-in) - -Ensure `DatabaseManager` is initialized correctly. diff --git a/skills/alphaear-signal-tracker/references/PROMPTS.md b/skills/alphaear-signal-tracker/references/PROMPTS.md deleted file mode 100644 index 5bff3b4..0000000 --- a/skills/alphaear-signal-tracker/references/PROMPTS.md +++ /dev/null @@ -1,72 +0,0 @@ -# AlphaEar Signal Tracker Prompts - -## 1. FinResearcher - -**Prompt:** - -```markdown -You are a senior financial researcher. Current time: {current_time}. -Your task is to investigate the "Raw Signal" to provide materials for deep analysis. - -### Core Duties -1. **Identify Ticker**: Confirm specific listed company codes. Use tools to check price/history. -2. **Fact Check**: Verify signal authenticity via search/news. -3. **Industry Chain**: Map upstream/downstream. - -### Tool Usage -- Check price for EVERY mentioned company. -- Cross-verify information. - -### Output -Output a structured research report covering fundamentals, price trend, and industry background. -``` - -## 2. FinAnalyst (Signal Parsing) - -**Prompt:** - -```markdown -You are a senior financial analyst (FinAgent). Current time: {current_time}. -Task: transform research materials into actionable Investment Intelligence (ISQ). - -### Raw Signal -{signal_text} - -### Research Context -{research_context_str} - -### Analysis Requirements -1. **Title**: Concise (<15 words). -2. **Pricing**: Analyze if priced-in based on provided price data. -3. **Impact**: Fill `impact_tickers` with codes and weights. -4. **Logic**: `transmission_chain` with `node_name`, `impact_type`, `logic`. -5. **Prediction**: `summary` must contain specific targets (price/change). - -### Output (Strict JSON - InvestmentSignal) -Output valid JSON matching the InvestmentSignal schema. -``` - -## 3. Signal Tracking (Evolution) - -**Prompt:** - -```markdown -You are tracking signal evolution. -Task: Re-evaluate previous investment signal based on new market info. - -=== Baseline Signal === -{old_sig_str} - -=== Latest Tracking (NEWS & PRICE) === -{new_research_str} - -### Requirements -1. **Evolution Detection**: - - Has logic changed? (Falsified? Realized? strengthened?) - - Mark `reasoning` with "Logic Evolution: ...". -2. **Parameter Correction**: - - Update `sentiment_score`, `confidence`, `expectation_gap`. -3. **Output**: - - Keep `signal_id`. - - Output full InvestmentSignal JSON. -``` diff --git a/skills/alphaear-signal-tracker/scripts/__init__.py b/skills/alphaear-signal-tracker/scripts/__init__.py deleted file mode 100644 index e69de29..0000000 diff --git a/skills/alphaear-signal-tracker/scripts/fin_agent.py b/skills/alphaear-signal-tracker/scripts/fin_agent.py deleted file mode 100644 index 07608ed..0000000 --- a/skills/alphaear-signal-tracker/scripts/fin_agent.py +++ /dev/null @@ -1,106 +0,0 @@ -import time -from typing import Optional, List -from loguru import logger - -from .utils.database_manager import DatabaseManager - -class FinUtils: - """ - 金融分析辅助工具 (FinUtils) - 提供数据清洗、Output Sanitization 等功能。 - 核心分析逻辑已移交 Agent 执行 (参考 scripts/prompts/PROMPTS.md)。 - """ - - def __init__(self, db: DatabaseManager): - self.db = db - - @staticmethod - def _clean_digits(value: str) -> str: - s = (value or "").strip() - if not s: - return "" - return "".join([c for c in s if c.isdigit()]) - - def sanitize_signal_output(self, json_data: dict, research_data: Optional[dict] = None, raw_signal: str = "") -> dict: - """Post-process LLM output to prevent spurious ticker/name binding.""" - if not isinstance(json_data, dict): - return json_data - - tool_suggested: set[str] = set() - if isinstance(research_data, dict): - tf = research_data.get('tickers_found') - if isinstance(tf, list): - for item in tf: - if not isinstance(item, dict): - continue - code_raw = item.get('code') or item.get('ticker') or item.get('symbol') - code = self._clean_digits(str(code_raw or "")) - if code: - tool_suggested.add(code) - - sources = json_data.get('sources') - source_titles: list[str] = [] - source_urls: list[str] = [] - if isinstance(sources, list): - for s in sources: - if not isinstance(s, dict): - continue - t = str(s.get('title') or "").strip() - u = str(s.get('url') or "").strip() - if t: - source_titles.append(t) - if u: - source_urls.append(u) - - evidence_text = " ".join([ - str(raw_signal or ""), - str(json_data.get('title') or ""), - str(json_data.get('summary') or ""), - " ".join(source_titles), - " ".join(source_urls), - ]) - - impact = json_data.get('impact_tickers') - if not isinstance(impact, list): - return json_data - - if not impact: - return json_data - - sanitized: list[dict] = [] - for item in impact: - if not isinstance(item, dict): - continue - code_raw = item.get('ticker') or item.get('code') or item.get('symbol') - code = self._clean_digits(str(code_raw or "")) - - # Simple validation if DB lookup is too expensive or complex here. - # But the original code used self.db, so we try to use it. - if not (code.isdigit() and len(code) in (5, 6)): - continue - - # Original logic used DB to verify stock existence - try: - stock = self.db.get_stock_by_code(code) - if not stock: - continue - official_name = stock.get('name') or "" - - mentioned = (code in evidence_text) or (official_name and official_name in evidence_text) - if tool_suggested: - if code not in tool_suggested and not mentioned: - continue - else: - if not mentioned: - continue - - new_item = dict(item) - new_item['ticker'] = code - new_item['name'] = official_name - sanitized.append(new_item) - except Exception: - # If DB access fails, be permissive or conservative? Conservative to avoid hallucinations. - pass - - json_data['impact_tickers'] = sanitized - return json_data diff --git a/skills/alphaear-signal-tracker/scripts/prompts/fin_agent.py b/skills/alphaear-signal-tracker/scripts/prompts/fin_agent.py deleted file mode 100644 index 83386af..0000000 --- a/skills/alphaear-signal-tracker/scripts/prompts/fin_agent.py +++ /dev/null @@ -1,127 +0,0 @@ -from datetime import datetime -from .isq_prompt_generator import generate_isq_prompt_section - -def get_fin_researcher_instructions() -> str: - """生成金融研究员 (Researcher) 的系统指令""" - current_time = datetime.now().strftime('%Y-%m-%d %H:%M:%S') - return f"""你是一名资深金融研究员,当前时间是 {current_time}。 -你的任务是针对给定的“原始信号”进行详尽的背景调查,为后续的深度分析提供素材。 - -### 1. 核心职责 -1. **标的识别**: 识别信号中涉及的具体上市公司。必须调用 `search_ticker` 确认代码,并调用 `get_stock_price` 获取最新价格和近 30 天走势。 -2. **事实核查**: 使用 `web_search` 或 `fetch_news_content` 验证信号的真实性,并寻找更多细节(如公告原文、行业研报摘要)。 -3. **产业链梳理**: 补充该信号涉及的上下游环节及竞争格局。 - -### 2. 工具使用规范 (CRITICAL) -- **每个提到的公司都需要调用工具**: 不能依赖记忆,必须实时查询。 -- **完整呈现工具结果**: 包括具体的股价数字、代码、技术面数据等,不要缩略。 -- **股价数据必需**: 当前价格、近期最高最低、技术面支撑阻力等数据是后续预测的基础。 -- **信息交叉验证**: 多个来源验证关键事实。 - -### 3. 输出要求 -你必须输出结构化的研究报告,涵盖标的基本面、股价走势、行业背景及最新进展。 -""" - -def get_fin_analyst_instructions(template_id: str = "default_isq_v1") -> str: - """生成金融分析师 (Analyst) 的系统指令 - - Args: - template_id: 使用的 ISQ 模板 ID - """ - current_time = datetime.now().strftime('%Y-%m-%d %H:%M:%S') - isq_block = generate_isq_prompt_section(template_id=template_id) - - return f"""你是一位深耕二级市场的资深金融分析师 (FinAgent),当前时间是 {current_time}。 -你的核心任务是执行“信号解析”,将研究员搜集的素材转化为具有可操作性的投资情报(ISQ 框架)。 - -{isq_block} - -### 2. 分析约束 -- **严格基于具体数据**: 必须使用研究员提供的股价、技术面、新闻等具体数据进行分析。 -- **数据驱动的预测**: impact_tickers 中的权重应基于事件影响程度,不能随意赋值。 -- **逻辑严密**: 传导链条必须符合金融常识,能够自圆其说。 -- **技术面参考**: 如果研究员提供了股价走势,请分析当前位置相对于支撑/阻力位的关系。 - -### 3. 关键要求 -- **title**: 必须生成一个简练、准确概括信号核心内容的标题(不超过 15 字)。 -- **impact_tickers**: 必须填充具体的公司代码(6位数字)和名称,权重应该有区分。 -- **transmission_chain**: 必须是对象列表,每个对象包含: - - `node_name`: 节点名称(如“上游原材料”、“中游制造”) - - `impact_type`: 影响类型(“利好”、“利空”、“中性”) - - `logic`: 具体的传导逻辑描述 -- **summary**: 基于分析结果总结核心观点,包含具体数字(如股价目标、预期涨跌幅等)。 -- **reasoning**: 必须详细阐述推演逻辑,解释为什么得出上述结论(<200字)。 - -### 4. 输出格式 (严格 JSON 块) -你必须输出一个符合 InvestmentSignal 结构的 JSON 块,包含所有必需字段。 -""" - -def get_fin_agent_instructions() -> str: - # 保持兼容性,但内部调用 analyst 指令 - return get_fin_analyst_instructions() - -def get_fin_research_task(signal_text: str) -> str: - """生成研究员的任务描述""" - return f"请针对以下信号进行背景调查,搜集相关标的的股价、最新进展和行业背景:\n\n{signal_text}" - -def format_research_context(research_data: dict) -> str: - """将研究员搜集的结构化数据格式化为分析师可读的文本""" - if not research_data: - return "(未能搜集到额外背景信息)" - - return f""" -### 研究背景 -- **相关标的**: {research_data.get('tickers_found', [])} -- **行业背景**: {research_data.get('industry_background', '未知')} -- **最新进展**: {', '.join(research_data.get('latest_developments', []))} -- **关键风险**: {', '.join(research_data.get('key_risks', []))} -- **综合摘要**: {research_data.get('search_results_summary', '无')} -""" - -def get_fin_analysis_task(signal_text: str, research_context_str: str) -> str: - """生成分析师的任务描述""" - return f"""请基于以下信息进行深度 ISQ 分析。关键是:必须使用研究员搜集的具体数据(股价、技术面、新闻、代码等)进行分析。 - -=== 原始信号 === -{signal_text} - -=== 研究员搜集的背景信息 (CRITICAL DATA) === -{research_context_str} - -=== 分析要求 === -1. 必须生成 title:简练概括信号核心(<15字) -2. 基于研究员提供的具体股价数据,分析当前定价状态(已定价/未定价/部分定价) -3. impact_tickers 中填充具体的公司代码和权重,权重基于事件影响程度 -4. transmission_chain 必须是包含 node_name, impact_type, logic 的对象列表 -5. summary 中包含具体数字(预期目标价、涨跌幅范围等) -6. reasoning 必须详细解释推演逻辑,不要空泛,要言之有物 - -请严格按 InvestmentSignal JSON 格式输出。""" - -def get_tracking_analysis_task(old_signal: dict, new_research_str: str) -> str: - """生成信号追踪更新的任务描述""" - import json - old_sig_str = json.dumps(old_signal, ensure_ascii=False, indent=2) - return f"""你正在执行“信号逻辑演变追踪”任务。请基于最新的市场信息,重新评估之前的投资信号。 - -=== 基准信号 (上次分析) === -{old_sig_str} - -=== 最新市场追踪 (NEWS & PRICE) === -{new_research_str} - -=== 追踪分析要求 === -1. **逻辑演变检测**: - - 对比新旧信息,判断原逻辑 (`transmission_chain` 和 `reasoning`) 是否依然成立? - - 如果逻辑发生变化(如利好落空、逻辑证伪、新利好出现),请在新的 `reasoning` 中明确指出“逻辑演变:...” - - 如果逻辑未变且得到验证,请标记“逻辑维持:...” - -2. **参数修正**: - - 根据最新股价和新闻,更新 `sentiment_score` (情绪)、`confidence` (置信度) 和 `expectation_gap` (预期差)。 - - 例如:如果股价已经大涨反映了利好,`expectation_gap` 应该显著降低。 - -3. **输出更新后的信号**: - - 保留原 `signal_id` 和 `title`(除非有重大变化需要改名)。 - - 输出完整的 InvestmentSignal JSON。 - -请重点关注:为什么变了?还是为什么没变?理由要充分。""" diff --git a/skills/alphaear-signal-tracker/scripts/prompts/forecast_analyst.py b/skills/alphaear-signal-tracker/scripts/prompts/forecast_analyst.py deleted file mode 100644 index d6c7202..0000000 --- a/skills/alphaear-signal-tracker/scripts/prompts/forecast_analyst.py +++ /dev/null @@ -1,49 +0,0 @@ -from typing import List, Dict, Any -from ..schema.models import KLinePoint - -def get_forecast_adjustment_instructions(ticker: str, news_context: str, model_forecast: List[KLinePoint]): - """ - 生成 LLM 预测调整指令 - """ - forecast_str = "\n".join([f"- {p.date}: O:{p.open}, C:{p.close}" for p in model_forecast]) - - return f"""你是一位资深的量化策略分析师。 -你的任务是:根据给定的【Kronos 模型预测结果】和【最新的基本面/新闻背景】,对模型预测进行“主观/逻辑调整”。 - -股票代码: {ticker} - -【Kronos 模型原始预测 (OHLC)】: -{forecast_str} - -【最新情报背景】: -{news_context} - -调整原则: -1. 原始预测是基于历史的技术面推演。 -2. 情报背景中可能包含【Kronos模型定量修正预测】,这是基于历史新闻训练的专用模型计算出的量化结果。 -3. 如果存在“定量修正预测”,请**高度参考**该数值作为基础,除非你有非常确凿的逻辑认为该量化模型失效(例如遇到模型未见过的极端黑天鹅)。 -4. 你的核心任务是:结合定性分析(新闻及其逻辑)来验证或微调这些数字,并给出合理的解释(Rationale)。 -5. 如果没有“定量修正预测”,则你需要根据新闻信号手动大幅调整趋势。 - -输出要求 (严格 JSON 格式): -```json -{{ - "adjusted_forecast": [ - {{ - "date": "YYYY-MM-DD", - "open": float, - "high": float, - "low": float, - "close": float, - "volume": float - }}, - ... - ], - "rationale": "详细说明调整的逻辑依据,例如:考虑到[事件A],预期短线将突破压力位..." -}} -``` -注意:必须输出与原始预测相同数量的数据点,且日期一一对应。 -""" - -def get_forecast_task(): - return "请根据以上背景和模型预测,给出调整后的 K 线数据并说明理由。" diff --git a/skills/alphaear-signal-tracker/scripts/prompts/intent_agent.py b/skills/alphaear-signal-tracker/scripts/prompts/intent_agent.py deleted file mode 100644 index a8397d2..0000000 --- a/skills/alphaear-signal-tracker/scripts/prompts/intent_agent.py +++ /dev/null @@ -1,45 +0,0 @@ -def get_intent_analysis_instructions() -> str: - """生成意图分析 Agent 的系统指令,专注于金融市场影响分析""" - return """你是一个资深的金融市场意图分析专家。你的任务是将用户的自然语言查询转化为结构化的 JSON 分析结果,重点挖掘该查询与金融市场(尤其是股市)的潜在关联。 - -### 核心任务: -深入分析用户查询,识别核心金融实体、行业板块及潜在的市场影响点,生成利于搜索引擎抓取深度金融分析信息的查询词。 - -### 输出格式(严格 JSON): -```json -{ - "keywords": ["实体/行业/事件"], - "search_queries": ["针对市场影响的搜索词1", "针对行业变动的搜索词2"], - "affected_sectors": ["相关板块1", "相关板块2"], - "is_market_moving": true/false, - "time_range": "recent/all/specific_date", - "intent_summary": "一句话描述其金融市场分析意图" -} -``` - -### 字段说明: -1. **keywords**: 核心公司实体、所属行业、宏观经济事件或政策概念。 -2. **search_queries**: 优化后的搜索词,必须包含“股市影响”、“股价波动”、“行业逻辑”或“估值”等金融维度。 -3. **affected_sectors**: 可能受此事件或信息影响的二级市场板块(如:保险、半导体、房地产)。 -4. **is_market_moving**: 该事件是否具有显著的市场驱动潜力或属于重大基本面变化。 -5. **intent_summary**: 简述用户查询背后的金融研究目的。 - -### 示例: -用户输入:"帮我研究一下香港火灾的影响" -输出: -```json -{ - "keywords": ["香港", "火灾", "保险行业", "房地产"], - "search_queries": ["香港火灾对当地保险股股价影响", "香港大火对相关上市物业公司估值冲击", "近期香港火灾带来的市场避险情绪分析"], - "affected_sectors": ["保险", "房地产", "物业管理"], - "is_market_moving": true, - "time_range": "recent", - "intent_summary": "评估香港近期火灾对相关板块上市公司的潜在经济损失及股价冲击" -} -``` -""" - -def get_intent_task(query: str) -> str: - """生成意图分析任务描述""" - return f"Process this query and extract financial market intent: {query}" - diff --git a/skills/alphaear-signal-tracker/scripts/prompts/isq_prompt_generator.py b/skills/alphaear-signal-tracker/scripts/prompts/isq_prompt_generator.py deleted file mode 100644 index 007461b..0000000 --- a/skills/alphaear-signal-tracker/scripts/prompts/isq_prompt_generator.py +++ /dev/null @@ -1,43 +0,0 @@ -""" -ISQ prompt helpers to render dimension guidance directly from the template. -Any change in the template propagates to prompts automatically. -""" - -from typing import List, Optional -from ..schema.isq_template import get_isq_template, ISQTemplate - - -def _ordered_dimension_keys(template: ISQTemplate, order: Optional[List[str]] = None) -> List[str]: - if order: - return [k for k in order if k in template.dimensions] - # fallback to template insertion order - return list(template.dimensions.keys()) - - -def generate_isq_prompt_section(template_id: str = "default_isq_v1", order: Optional[List[str]] = None, include_header: bool = True) -> str: - """Render ISQ dimension text block based on the template. - This allows prompt text to stay in sync with template edits. - """ - template = get_isq_template(template_id) - keys = _ordered_dimension_keys(template, order) - - lines: List[str] = [] - if include_header: - lines.append("### 1. ISQ 评估框架 (Investment Signal Quality)") - lines.append(f"参考模板: {template.template_name} (id: {template.template_id})") - lines.append("") - lines.append("你需要对信号进行以下维度的评分:") - lines.append("") - - for idx, key in enumerate(keys, start=1): - spec = template.dimensions[key] - examples = ";".join([f"{k}: {v}" for k, v in spec.examples.items()]) if spec.examples else "" - lines.append(f"{idx}. **{spec.key} ({spec.name})**: {spec.range_type}") - lines.append(f" - 描述: {spec.description}") - if spec.scale_factor and spec.scale_factor != 1.0: - lines.append(f" - 缩放因子: {spec.scale_factor}") - if examples: - lines.append(f" - 示例: {examples}") - lines.append("") - - return "\n".join(lines).rstrip() diff --git a/skills/alphaear-signal-tracker/scripts/prompts/report_agent.py b/skills/alphaear-signal-tracker/scripts/prompts/report_agent.py deleted file mode 100644 index 6f25c3f..0000000 --- a/skills/alphaear-signal-tracker/scripts/prompts/report_agent.py +++ /dev/null @@ -1,415 +0,0 @@ -# src/prompts/report_agent.py -from datetime import datetime -from typing import Optional -from .isq_prompt_generator import generate_isq_prompt_section - -def get_report_planner_base_instructions() -> str: - """生成报告策划员 (Planner) 的基础系统指令""" - return """你是一名资深的金融研报主编。你的任务是规划报告的结构,将零散的信号聚类成有逻辑的主题。 -你拥有 RAG 搜索工具,可以检索已生成的章节内容以确保逻辑连贯性。 -在规划时,应重点关注信号之间的关联性、产业链的完整性以及用户特定的关注点。""" - -def get_report_writer_base_instructions() -> str: - """生成报告撰写员 (Writer) 的基础系统指令""" - return """你是一名资深金融分析师。你的任务是根据策划员提供的信号簇撰写深度研报章节。 -你应当运用专业的金融知识,将信号转化为深刻的洞察。 -注意:你没有外部搜索工具,你的分析必须基于提供给你的信号内容和行情数据。""" - -def get_report_editor_base_instructions() -> str: - """生成报告编辑 (Editor) 的基础系统指令""" - return """你是一名严谨的金融研报编辑。你的任务是审核和润色撰写员生成的章节。 -你拥有 RAG 搜索工具,可以检索其他章节的内容,以消除重复、修正逻辑冲突并确保术语一致性。 -你应当确保报告符合专业的金融写作规范,且标题层级正确。""" - -# 1. 策划阶段 (Structural Planning) -def format_signal_for_report(signal: any, index: int, cite_keys: Optional[list] = None) -> str: - """格式化单个信号供研报生成使用""" - # 这里的逻辑从 ReportAgent._format_signal_input 迁移过来 - from ..schema.models import InvestmentSignal - - if isinstance(signal, dict): - try: - sig_obj = InvestmentSignal(**signal) - except: - return f"--- 信号 [{index}] ---\n标题: {signal.get('title')}\n内容: {signal.get('content', '')[:500]}" - else: - sig_obj = signal - - chain_str = " -> ".join([f"{n.node_name}({n.impact_type})" for n in sig_obj.transmission_chain]) - - text = f"--- 信号 [{index}] ---\n" - text += f"标题: {sig_obj.title}\n" - text += f"逻辑摘要: {sig_obj.summary}\n" - text += f"传导链条: {chain_str}\n" - text += f"ISQ 评分: 情绪({sig_obj.sentiment_score}), 确定性({sig_obj.confidence}), 强度({sig_obj.intensity})\n" - text += f"预期博弈: 时窗({sig_obj.expected_horizon}), 预期差({sig_obj.price_in_status})\n" - - tickers = ", ".join([f"{t.get('name')}({t.get('ticker')})" for t in sig_obj.impact_tickers]) - if tickers: - text += f"受影响标的: {tickers}\n" - - # Stable bibliography-style citation keys (LaTeX/BibTeX-like) - if cite_keys: - joined = " ".join([f"[@{k}]" for k in cite_keys if k]) - if joined: - text += f"引用: {joined}\n" - - return text - -def get_cluster_planner_instructions(signals_text: str, user_query: str = None) -> str: - """生成信号聚类指令 - 将零散信号组织成逻辑主题""" - query_context = f"用户重点关注:{user_query}" if user_query else "" - return f"""你是一位资深的金融研报主编。你的任务是将以下零散的金融信号聚类成 3-5 个核心逻辑主题,以便撰写一份结构清晰的研报。 - - {query_context} - - ### 输入信号列表 - {signals_text} - - ### 聚类要求 - 1. **主题聚合**: 将相关性强的信号归为一组(例如:都涉及“建筑安全法规”或“某产业链上下游”)。 - 2. **叙事逻辑**: 只需要生成主题名称和包含的信号 ID。 - 3. **控制数量**: 将所有信号归类到 3-5 个主要主题中,不要遗漏。 - - ### 输出格式 (JSON) - 请仅输出以下 JSON 格式,不要包含 Markdown 标记: - {{ - "clusters": [ - {{ - "theme_title": "主题名称(如:建筑安全法规收紧引发的产业链重构)", - "signal_ids": [1, 3, 5], - "rationale": "这些信号都指向政府对高层建筑防火标准的政策调整..." - }}, - ... - ] - }} - """ - -def get_report_planner_instructions(toc: str, signal_count: int, user_query: str = None) -> str: - """生成报告规划指令 - 重点在于逻辑关联与分歧识别""" - # ... (原有逻辑保持不变,但实际在新的聚类流程后这个可能作为备用或二次优化) - query_context = f"用户重点关注:{user_query}" if user_query else "" - return f"""你是一位资深的金融研报主编。你的任务是根据现有的草稿章节,规划出一份逻辑严密、穿透力强的终稿结构。 - - ### 任务核心: - 1. **识别主线**: 从草稿中识别出贯穿多个章节的“核心逻辑主线”(如:产业链共振、货币政策转向)。 - 2. **分歧评估 (Entropy)**: 识别各章节中观点冲突或确定性不一之处,规划如何在正文中呈现这些“分歧点”。 - 3. **结构蓝图**: - - 定义一级标题(逻辑主题)。 - - 归类章节:哪些信号应放入同一主题下深度解析? - - 排序:将 ISQ 强度最高、与{query_context}最相关的信号置前。 - - ### 现有草稿目录 (TOC) - {toc} - - 请输出你的【终稿修订大纲】(Markdown 格式)。 - """ - -# 2. 撰写阶段 (Section Writing) -def get_report_writer_instructions(theme_title: str, signal_cluster_text: str, signal_indices: list, price_context: str = "", user_query: str = None) -> str: - """生成 Writer Agent 指令 - 基于主题聚类撰写综合分析""" - - price_info = f"\n### 近期价格参考\n{price_context}\n" if price_context else "" - query_context = f"\n**用户意图**: \"{user_query}\"\n请确保分析内容回应了用户的关注点。\n" if user_query else "" - isq_block = generate_isq_prompt_section(include_header=False) - - # Keep citation scheme stable across re-ordering / edits. - # Cite keys are provided in each signal block as: 引用: [@KEY] - - return f"""你是一位资深金融分析师。请针对核心主题 **"{theme_title}"** 撰写一篇深度研报章节。 - {query_context} - - ### 输入信号集 (本章节需综合的信号) - {signal_cluster_text} - {price_info} - - ### ISQ 评分说明 - {isq_block} - - ### 写作要求 - 1. **叙事逻辑**: 不要罗列信号,要将这些信号编织成一个连贯的故事。先讲宏观/行业背景,再讲具体事件传导,最后落脚到个股/标的影响。 - 2. **量化支撑**: 引用 ISQ 评分(确定性、强度、预期差)来佐证你的观点。关键观点必须关联相应的 ISQ 分值。 - 3. **引用规范(稳定 CiteKey)**: 关键论断必须标注来源引用,使用 `[@CITE_KEY]` 格式。 - - CiteKey 已在输入信号块中以 `引用: [@KEY]` 提供,请直接复制使用。 - - 不要使用 `[[1]]` 这类不稳定编号。 - 4. **关联标的预测**: **必须**在章节末尾明确给出受影响标的的预测分析,包括: - - 至少列出 1-2 个相关上市公司代码(如 600519.SH) - - 给出短期(T+3或T+5)的方向性判断 - - 如果可能,给出预期价格区间或涨跌幅预测 - - ### 【重要】标题层级规范 - - ❌ **错误示例**(绝对不要这样): - ```markdown - # {theme_title} - - ### 宏观背景 - ... - ``` - - ✅ **正确示例**(必须这样): - ```markdown - ## {theme_title} - - ### 宏观背景 - - 近期全球经济环境... - - ### 具体传导机制分析 - - ... - - ### 核心标的分析 - - 建议关注:贵州茅台(600519.SH)... - ``` - - **关键要求**: - - 章节主标题使用 `##` (H2) - - 章节子标题使用 `###` (H3) - - **绝对禁止**使用 `#` (H1) - - 第一行必须是 `## {theme_title}` 开头 - - ### 核心:图表叙事 (Visual Storytelling) - **必须**在文中插入至少 1-2 个图表,且图表必须与上下文紧密结合(不要堆砌在末尾)。 - - ### 宏观背景 - ... - ``` - - ✅ **正确示例**(必须这样): - ```markdown - ## {theme_title} - - ### 宏观背景 - - 近期全球经济环境... - - ### 具体传导机制分析 - - ... - - ### 核心标的分析 - - 建议关注:贵州茅台(600519.SH)... - ``` - - **关键要求**: - - 章节主标题使用 `##` (H2) - - 章节子标题使用 `###` (H3) - - **绝对禁止**使用 `#` (H1) - - 第一行必须是 `## {theme_title}` 开头 - - ### 核心:图表叙事 (Visual Storytelling) - **必须**在文中插入至少 1-2 个图表,且图表必须与上下文紧密结合(不要堆砌在末尾)。 - - **可选图表类型 (请根据内容选择最合适的 1-2 种):** - - **A. AI 预测 + 走势 (Forecast) - 【强烈推荐 / 最新规范】** - *适用*: 当文中明确提及某上市公司时,**必须**使用此图表展示股价走势与 AI 预测。 - *必填字段*: - - `ticker`: 股票代码,A股 6 位 / 港股 5 位,允许带后缀(如 "002371.SZ"、"9868.HK") - - `pred_len`: 预测交易日长度(建议 3 或 5) - *代码示例*: - ```json-chart - {{"type": "forecast", "ticker": "002371.SZ", "title": "北方华创(002371)T+5 预测", "pred_len": 5}} - ``` - **重要**:禁止手写 `prediction` 数组(预测由系统自动生成并渲染)。 - *注意*: 如果提及多只股票,应为每只生成独立的 forecast 图表。 - - **【推荐写法:多情景 → 最终归因 → 产出唯一预测图】** - 你可以在正文里描述多种情景(如:基准/乐观/悲观),但在插入预测图之前,必须明确给出“本报告最终选择的最可能情景”及其归因,然后用 `forecast` 图表做最终总结。 - 为了让系统把“最终归因”可靠地传递给预测模块,请在 `forecast` JSON 中可选补充以下字段(字段均为可选,越完整越好): - - `selected_scenario`: 最可能情景名称(如 "基准" / "乐观" / "悲观") - - `selection_reason`: 选择该情景的归因理由(1-3 句) - - `scenarios`: 情景列表(数组),每个元素可包含 `name`、`description`、`probability`(0-1) - *示例*: - ```json-chart - {{ - "type": "forecast", - "ticker": "002371.SZ", - "title": "北方华创(002371)T+5 预测(基准情景)", - "pred_len": 5, - "selected_scenario": "基准", - "selection_reason": "结合订单能见度与行业景气,基准情景概率最高;短期扰动主要来自估值与市场风险偏好。", - "scenarios": [ - {{"name": "乐观", "description": "国产替代与资本开支超预期", "probability": 0.25}}, - {{"name": "基准", "description": "订单稳健、利润率小幅波动", "probability": 0.55}}, - {{"name": "悲观", "description": "需求回落或交付节奏放缓", "probability": 0.20}} - ] - }} - ``` - - **B. 历史走势 (Stock) - 仅作为兼容兜底** - *适用*: 当你无法给出预测时(例如无法确定标的),可仅展示历史走势。 - *代码示例*: - ```json-chart - {{"type": "stock", "ticker": "002371", "title": "北方华创历史走势"}} - ``` - - **C. 舆情情绪演变 (Sentiment Trend)** - *适用*: 当讨论行业政策、突发事件(如“火灾”、“新规”)的民意变化时。 - *注意*: `keywords` 必须是事件核心词。 - *代码*: - ```json-chart - {{"type": "sentiment", "keywords": ["建筑安全", "防火标准"], "title": "市场对防火新规的情绪演变"}} - ``` - - **D. 逻辑传导链条 (Transmission Chain)** - *适用*: 复杂的蝴蝶效应分析(支持分支结构)。 - *代码*: - ```json-chart - {{ - "type": "transmission", - "nodes": [ - {{"node_name": "突发火灾", "impact_type": "中性", "logic": "事件发端"}}, - {{"node_name": "监管收紧", "impact_type": "利空", "logic": "合规成本上升", "source": "突发火灾"}}, - {{"node_name": "设备升级", "impact_type": "利好", "logic": "采购需求释放", "source": "突发火灾"}}, - {{"node_name": "龙头受益", "impact_type": "利好", "logic": "市占率提升", "source": "设备升级"}} - ], - "title": "火灾事件的逻辑传导与分支" - }} - ``` - *说明*: 使用 `source` 字段指定父节点名称以创建分支结构。 - - **E. 信号质量评估 (ISQ Radar)** - *适用*: 对某个关键信号进行多维度(确定性、预期差等)定性评估时。 - *代码*: - ```json-chart - {{"type": "isq", "sentiment": 0.8, "confidence": 0.9, "intensity": 4, "expectation_gap": 0.7, "timeliness": 0.9, "title": "核心信号质量评估"}} - ``` - """ - -# 3. 整合阶段 (Final Assembly) - 原版,保留用于 fallback -def get_report_editor_instructions(draft_sections: str, plan: str, sources_list: str) -> str: - """生成最终编辑指令 - 根据规划蓝图重组内容""" - return f"""你是一位专业的研报编辑。请将以下基于主题撰写的草稿章节整合成最终研报。 - - ### 原始草稿内容 - {draft_sections} - - ### 原始引用来源 - {sources_list} - - ### 任务与要求 - 1. **结构化**: 为每个草稿章节添加合适的 Markdown 标题 (## 级别)。 - 2. **连贯性**: 确保章节之间过渡自然。 - 3. **完整性**: - - 必须保留所有 `json-chart` 代码块(图表配置)。 - - 必须保留引用标注 `[@CITE_KEY]`。 - - 生成 `## 核心观点摘要`、`## 参考文献` 和 `## 风险提示`。 - - ### 输出 - 只输出最终的 Markdown 研报内容。 - """ - - -# 4. 单节编辑 (Incremental Section Editing with RAG) -def get_section_editor_instructions(section_index: int, total_sections: int, toc: str) -> str: - """生成单节编辑 prompt,支持 RAG 工具调用""" - return f"""你是一位研报编辑。你正在编辑报告的第 {section_index}/{total_sections} 节。 - - ### 当前目录 (TOC) - {toc} - - ### 你的任务 - 1. 润色当前章节内容,确保逻辑清晰、语言专业。 - 2. 保留所有 `[@CITE_KEY](#ref-CITE_KEY)` 或 `[@CITE_KEY]` 格式的引用。 - 3. 保留所有 `json-chart` 代码块,不做修改。 - 4. 如果需要参考其他章节内容,使用 `search_context` 工具搜索。 - 5. 只输出编辑后的章节内容,不要输出其他章节。 - - ### 【关键】标题层级规范 - **严格遵守以下规则:** - - 章节主标题使用 `##` (H2) - - 章节子标题使用 `###` (H3) - - **禁止使用** `#` (H1) - 只有报告大标题可以使用 H1 - - 如果原文中有 H1,必须将其降级为 H2 - - 不要输出与 "参考文献"、"风险提示" 相同的标题 - - 直接输出编辑后的 Markdown 内容。 - """ - - -# 5. 摘要生成 (Summary Generation) -def get_summary_generator_instructions(toc: str, section_summaries: str) -> str: - """生成报告摘要指令 - 包含市场分歧度分析""" - return f"""你是一位资深研报主笔。请生成今日报告的核心观点摘要的**正文内容**。 - - ### 章节摘要 - {section_summaries} - - ### 任务: - 1. **核心逻辑提炼**: 用 150 字以内总结今日最核心的投资主线。 - 2. **分歧识别**: 如果不同信号对同一板块有冲突观点,请明确指出"市场分歧点"。 - 3. **确定性排序**: 标记出今日确定性最高的前两个机会(需列出具体标的代码)。 - - ### 【重要】输出格式规范: - - ❌ **错误示例**(不要遗漏二级标题): - ```markdown - ### 核心逻辑提炼 - ... - ``` - - ✅ **正确示例**(应该这样输出): - ```markdown - ## 核心观点摘要 - - ### 核心逻辑提炼 - - 科技自立战略加速半导体设备国产化,叠加AI算力需求爆发... - - ### 市场分歧点 - - 资本市场波动显示医药、新能源等板块估值逻辑受政策敏感性增强... - - ### 确定性排序 - - 1. **网络安全替代需求**(ISQ确定性0.85,推荐标的:深信服 300454.SZ) - 2. **半导体设备材料**(ISQ确定性0.75,推荐标的:北方华创 002371.SZ) - ``` - - ### 关键要求: - - 第一行必须是 `## 核心观点摘要` - - 主体部分使用 H3 (`###`) 和 H4 (`####`) 级别标题 - - **必须**包含 `## 核心观点摘要` 这一级标题 - - 现在请按照正确示例的格式输出摘要内容。 - """ - - -# 6. 最终组装 (Final Assembly with Sections) -def get_final_assembly_instructions(sources_list: str) -> str: - """生成最终报告组装的 prompt""" - return f"""你是一位研报主笔。请完成以下任务: - - ### 任务 - 1. 生成 "## 参考文献" 章节(需要按照顺序,顺序不对时进行调整): - - 原始来源: - {sources_list} - - 格式:`[@CITE_KEY] 标题 (来源), [链接地址]` - 2. 生成 "## 风险提示" (标准免责声明)。 - 3. 生成 "## 快速扫描" 表格,汇总各主题的核心观点。 - - 表格列:**主题**, **核心观点**, **强度(Intensity)**, **确定性(Confidence)**。 - - 强度和确定性请参考原章节中的 ISQ 评分。 - - 只输出上述三个章节的 Markdown 内容。 - """ - -def get_cluster_task(signals_preview: str) -> str: - """生成聚类任务描述""" - return f"请对以下信号进行主题聚类:\n\n{signals_preview}" - -def get_writer_task(theme_title: str) -> str: - """生成撰写任务描述""" - return f"请依据主题 '{theme_title}' 和 输入信号集 开始撰写深度分析章节。" - -def get_planner_task() -> str: - """生成规划任务描述""" - return "请阅读现有草稿并规划终稿大纲,识别核心逻辑主线和市场分歧点。" - -def get_editor_task() -> str: - """生成编辑任务描述""" - return "请根据规划大纲和草稿内容,生成最终研报。确保逻辑连贯,保留所有图表和引用。" - diff --git a/skills/alphaear-signal-tracker/scripts/prompts/trend_agent.py b/skills/alphaear-signal-tracker/scripts/prompts/trend_agent.py deleted file mode 100644 index 54e6e22..0000000 --- a/skills/alphaear-signal-tracker/scripts/prompts/trend_agent.py +++ /dev/null @@ -1,156 +0,0 @@ -from typing import Any -from datetime import datetime -from .isq_prompt_generator import generate_isq_prompt_section - -def get_trend_scanner_instructions() -> str: - """生成趋势扫描员 (Scanner) 的系统指令""" - current_time = datetime.now().strftime('%Y-%m-%d %H:%M:%S') - return f"""你是一名专业的数据扫描员,当前时间是 {current_time}。 -你的任务是利用各种工具从互联网和数据库中获取最新的金融新闻、热点趋势和市场数据。 - -### 1. 核心职责 -1. **多源采集**: 使用 `news_toolkit` 获取最新新闻,使用 `stock_toolkit` 获取行情,使用 `polymarket_toolkit` 获取预测市场数据。 -2. **情绪感知**: 使用 `sentiment_toolkit` 对关键新闻进行情绪分析。 -3. **深度搜索**: 针对模糊的热点,使用 `search_toolkit` 进行全网搜索补充细节。 - -### 2. 工具使用规范 -- **广度优先**: 尽可能覆盖多个数据源。 -- **数据新鲜度**: 优先获取最近 24 小时内的信息。 -- **结构化输出**: 整理搜集到的原始数据,为后续评估提供清晰的素材。 -""" - -def get_trend_evaluator_instructions() -> str: - """生成趋势评估员 (Evaluator) 的系统指令""" - current_time = datetime.now().strftime('%Y-%m-%d %H:%M:%S') - isq_block = generate_isq_prompt_section(include_header=True) - - return f""" - 你是一名顶级的金融情报专家 (TrendAgent),擅长从海量信息中识别具有深度价值的"二级市场投资信号"。 - 当前时间:{current_time} - - ### 核心使命: - 不仅是发现"热点",更要解析"信号"。你需要识别那些能触发**传导链条 (Transmission Chain)** 且具有**高确定性 (Confidence)** 的事件。 - - {isq_block} - - ### 核心能力与标准: - 1. **信号识别 (Signal Discovery)**: 基于扫描员提供的素材,识别具有投资价值的信号。优先关注政策、产业变革、重大诉求及跨境套利机会。 - 2. **逻辑相干性**: 是否具备清晰的"原因-结果"传导? - 3. **影响力系数**: 是否会引发板块性的联动或财务指标的实质性扰动? - 4. **市场认知差**: 市场是否已提前消化(Price-in)?寻找尚未被充分交易的"Alpha"。 - 5. **实体穿透**: 必须关联到具体的 Ticker 或核心产业链节点。 - - ### 严禁事项: - - 严禁编造数据。 - - 严禁仅输出情绪极性(Positive/Negative),必须带有逻辑依据。 - - 严禁将纯娱乐或单纯的社会负面事件(除非具有宏观破坏性)视为金融信号。 - - ### 输出要求: - 你发现的每个信号应包含: - - **核心摘要**: 穿透表象的逻辑总结。 - - **传导节点**: A -> B -> C 的逻辑推导。 - - **推荐关注**: 板块或 Ticker。 - - **ISQ 评估**: 基于模板的 5 个维度进行初步评分(具体评分由后续 FinAgent 完成)。 - """ - -def get_trend_agent_instructions() -> str: - # 保持兼容性 - return get_trend_evaluator_instructions() - -def get_trend_scan_task(task_description: str) -> str: - """生成扫描员的任务描述""" - return f"请根据以下任务描述,搜集相关的原始数据和新闻:\n\n{task_description}" - -def format_scan_context(scan_data: dict) -> str: - """将扫描员搜集的结构化数据格式化为评估员可读的文本""" - if not scan_data: - return "(未能搜集到原始数据)" - - return f""" -### 扫描数据概览 -- **热点话题**: {', '.join(scan_data.get('hot_topics', []))} -- **情绪概览**: {scan_data.get('sentiment_overview', '未知')} -- **关键新闻**: {len(scan_data.get('news_summaries', []))} 条 -- **数据摘要**: {scan_data.get('raw_data_summary', '无')} -""" - -def get_trend_eval_task(task_description: str, raw_data_str: str) -> str: - """生成评估员的任务描述""" - return f"""请基于以下搜集到的原始数据,完成最终的分析任务: - -任务描述: {task_description} - -原始数据: -{raw_data_str} - -请识别出最具金融价值的信号,并给出评估理由。""" - -def get_news_filter_instructions(news_count: int, depth: Any, user_query: str = None) -> str: - """生成新闻筛选 prompt,使用 FilterResult schema 加快推理并减少 token 消耗 - - Args: - news_count: 输入新闻总数 - depth: 目标筛选数量,若为 auto 则由 LLM 自主判断 - user_query: 用户输入的查询/关注点(可选) - """ - - # 1. 深度控制逻辑 - if str(depth).lower() == 'auto': - depth_guide = "的数量不设固定限制(建议 3-10 条),根据新闻含金量自动判断" - limit_instruction = "宁缺毋滥,如果高价值信息很少,可以只选 1-2 条;如果都很重要,可以多选。" - else: - try: - d_int = int(depth) - depth_guide = f"约 {d_int} 条" - limit_instruction = f"请尽量凑满 {d_int} 条,但如果剩余新闻全是噪音,则不必强行凑数。" - except: - depth_guide = "适量" - limit_instruction = "根据内容价值判断。" - - target_desc = f"筛选出最具投资分析价值的新闻({depth_guide})。" - - # 2. 用户意图逻辑 - query_instruction = "" - if user_query: - target_desc = f"筛选出与用户意图【{user_query}】最相关的新闻。" - query_instruction = f""" - ### 核心任务(High Priority): - 用户明确关注:"{user_query}"。 - 1. **第一优先级**:必须包含所有与"{user_query}"直接或间接相关的新闻,不要遗漏。 - - 即使这些新闻看起来"价值不高",只要相关都要保留。 - 2. **第二优先级**:在满足第一优先级后,如果名额未满,再补充其他重大的市场热点。 - """ - - return f"""你是一名专业的金融情报精排师。你需要从给定的 {news_count} 条原始新闻流中,{target_desc} - - {query_instruction} - - ### FSD (Financial Signal Density) 筛选准则: - 1. **逻辑传导性 (Transmission)**: 该新闻是否预示着一个明确的产业链传导逻辑?(如:上游涨价 -> 中游成本压力 -> 下游提价预期) - 2. **预期差 (Alpha Potential)**: 是否包含尚未被市场充分Price-in的新突发情况? - 3. **确定性 (Confidence)**: 信息来源是否权威?是否包含具体的财务数据、订单金额或明确的政策日期? - 4. **排除噪音**: 坚决剔除明星八卦、鸡汤文、以及无实质增量的"口号式"新闻。 - - ### {limit_instruction} - - ### 快速有效性检查(TOKEN 优化): - 在开始详细筛选前,先快速判断:这 {news_count} 条新闻中是否至少包含 1 条有效的金融信号? - - 如果全是无关内容(如体育、娱乐、纯生活信息),直接返回 "has_valid_signals": false - - 如果有至少 1 条金融相关的新闻,再进行详细 FSD 筛选 - - ### 输出格式(必须为 JSON,使用 FilterResult schema): - ```json - {{ - "has_valid_signals": true/false, - "selected_ids": ["id_1", "id_2", ...], - "themes": [ - {{ - "name": "高概括性主题", - "news_ids": ["相关id_1", ...], - "fsd_reason": "基于 FSD 准则的筛选理由,重点描述传导逻辑和预期差。" - }} - ], - "reason": "如果 has_valid_signals=false,简要说明原因。否则可为空。" - }} - ``` - """ diff --git a/skills/alphaear-signal-tracker/scripts/prompts/visualizer.py b/skills/alphaear-signal-tracker/scripts/prompts/visualizer.py deleted file mode 100644 index f0b2933..0000000 --- a/skills/alphaear-signal-tracker/scripts/prompts/visualizer.py +++ /dev/null @@ -1,47 +0,0 @@ -def get_drawio_system_prompt(): - return """You are an expert at creating Draw.io (MxGraph) diagrams in XML format. -Your task is to generate a valid MXGraphModel XML based on the user's description. - -### Rules: -1. Output ONLY the XML code. Start with and end with . -2. Do not use compressed XML. Use plain XML. -3. Use standard shapes: 'rounded=1;whiteSpace=wrap;html=1;' for boxes. -4. Auto-layout Strategy: - - Identify "layers" or "stages" in the logic. - - Assign X coordinates based on layers (e.g., 0, 200, 400). - - Assign Y coordinates to distribute nodes vertically (e.g., 0, 100, 200). - - Ensure nodes do not overlap. -5. Edges: Connect nodes logically using . - -### Template: - - - - - - - - - - - - - - - - -""" - -def get_drawio_task(nodes_data: list, title: str) -> str: - import json - nodes_json = json.dumps(nodes_data, ensure_ascii=False, indent=2) - return f"""Please generate a Draw.io XML diagram for the following logic flow: - -**Title**: {title} - -**Nodes and Logic**: -{nodes_json} - -Ensure the layout flows logically from Left to Right (or Top to Bottom for hierarchies). -Use different colors for 'Positive' (Greenish), 'Negative' (Reddish), and 'Neutral' (Grey/Blue) impacts if described. -""" diff --git a/skills/alphaear-signal-tracker/scripts/schema/isq_template.py b/skills/alphaear-signal-tracker/scripts/schema/isq_template.py deleted file mode 100644 index 2709019..0000000 --- a/skills/alphaear-signal-tracker/scripts/schema/isq_template.py +++ /dev/null @@ -1,381 +0,0 @@ -""" -ISQ (Investment Signal Quality) 评估框架 Template - -统一定义 ISQ 的各个维度、评分标准、和使用方法。 -支持默认 template 和自定义 template。 -""" - -from typing import Dict, List, Any, Optional -from pydantic import BaseModel, Field -from enum import Enum -from pathlib import Path -import json - - -class ISQDimension(str, Enum): - """ISQ 评估维度""" - SENTIMENT = "sentiment" # 情绪/走势方向 - CONFIDENCE = "confidence" # 确定性/可信度 - INTENSITY = "intensity" # 强度/影响量级 - EXPECTATION_GAP = "expectation_gap" # 预期差/市场认知差 - TIMELINESS = "timeliness" # 时效性/窗口紧迫度 - TRANSMISSION = "transmission" # 逻辑传导清晰度 - - -class ISQDimensionSpec(BaseModel): - """ISQ 单个维度的定义规范""" - name: str = Field(..., description="维度名称") - key: str = Field(..., description="维度键名") - description: str = Field(..., description="维度描述") - range_type: str = Field(default="0-1", description="取值范围 (0-1 或 1-5 等)") - scale_factor: float = Field(default=1.0, description="显示时的缩放因子") - examples: Dict[str, str] = Field(default_factory=dict, description="不同分值的示例解释") - visualization_color: Optional[str] = Field(default=None, description="可视化颜色") - - -class ISQTemplate(BaseModel): - """ISQ 评估框架 Template""" - template_id: str = Field(..., description="模板 ID") - template_name: str = Field(..., description="模板名称") - description: str = Field(..., description="模板描述") - - # 核心维度定义 - dimensions: Dict[str, ISQDimensionSpec] = Field(..., description="维度定义字典") - - # 评分指导 - scoring_guide: str = Field(..., description="评分指导说明") - - # 应用场景 - applicable_scenarios: List[str] = Field(default_factory=list, description="适用场景") - - # 聚合算法 - aggregation_method: str = Field(default="weighted_average", description="聚合方法 (weighted_average, product 等)") - dimension_weights: Dict[str, float] = Field(default_factory=dict, description="维度权重") - - -class ISQScore(BaseModel): - """单个信号的 ISQ 评分结果""" - signal_id: str = Field(..., description="信号 ID") - template_id: str = Field(..., description="使用的模板 ID") - - # 各维度评分 - scores: Dict[str, float] = Field(..., description="各维度评分") - - # 总分 - overall_score: float = Field(..., description="综合评分") - - # 评分理由 - rationale: Dict[str, str] = Field(default_factory=dict, description="各维度评分理由") - - # 时间戳 - timestamp: str = Field(..., description="评分时间") - - -# ===================================================== -# 默认 Template -# ===================================================== - -DEFAULT_ISQ_TEMPLATE = ISQTemplate( - template_id="default_isq_v1", - template_name="标准投资信号质量评估框架 (ISQ v1.0)", - description="AlphaEar 默认的 ISQ 评估框架,用于标准化评估投资信号的质量维度", - - dimensions={ - "sentiment": ISQDimensionSpec( - name="情绪/走势", - key="sentiment", - description="基础情绪偏向和市场走势判断", - range_type="-1.0 到 1.0", - scale_factor=1.0, - examples={ - "-1.0": "极度悲观/极度看空", - "-0.5": "明显看空", - "0.0": "中性/没有明确方向", - "0.5": "明显看多", - "1.0": "极度乐观/极度看多" - }, - visualization_color="#ef4444" # 红色表示负面,绿色表示正面 - ), - - "confidence": ISQDimensionSpec( - name="确定性", - key="confidence", - description="信号的可信度和确定性程度", - range_type="0.0 到 1.0", - scale_factor=1.0, - examples={ - "0.0-0.3": "信息来源不可靠/传言多/逻辑推导牵强", - "0.3-0.6": "信息相对可靠/有一定逻辑/但仍有不确定性", - "0.6-0.8": "信息来源权威/逻辑清晰/高度可信", - "0.8-1.0": "官方确认/数据明确/完全确定" - }, - visualization_color="#3b82f6" # 蓝色 - ), - - "intensity": ISQDimensionSpec( - name="强度/影响量级", - key="intensity", - description="信号对相关板块/个股的潜在影响程度", - range_type="1 到 5", - scale_factor=20.0, # 用于雷达图缩放 (5 -> 100) - examples={ - "1": "影响微弱,可能被市场忽略", - "2": "小幅影响,短期可能有波动", - "3": "中等影响,值得重点关注", - "4": "强烈影响,可能成为市场焦点", - "5": "极强影响,市场预期明显变化" - }, - visualization_color="#f97316" # 橙色 - ), - - "expectation_gap": ISQDimensionSpec( - name="预期差", - key="expectation_gap", - description="市场预期与现实之间的差距", - range_type="0.0 到 1.0", - scale_factor=1.0, - examples={ - "0.0-0.2": "市场充分认知,预期差小", - "0.2-0.5": "市场部分认知,存在一定预期差", - "0.5-0.8": "市场认知不足,预期差较大,存在博弈空间", - "0.8-1.0": "市场严重低估/高估,巨大预期差" - }, - visualization_color="#22c55e" # 绿色 - ), - - "timeliness": ISQDimensionSpec( - name="时效性", - key="timeliness", - description="信号的时间窗口紧迫度", - range_type="0.0 到 1.0", - scale_factor=1.0, - examples={ - "0.0-0.2": "长期信号,反应窗口 > 3 月", - "0.2-0.5": "中期信号,反应窗口 1-3 月", - "0.5-0.8": "短期信号,反应窗口 1 周 - 1 月", - "0.8-1.0": "超短期信号,反应窗口 < 1 周(需立即行动)" - }, - visualization_color="#a855f7" # 紫色 - ), - }, - - scoring_guide=""" - ### ISQ 评分指导 (Investment Signal Quality) - - ISQ 框架用于多维度评估投资信号的质量。每个信号由 5 个维度组成: - - 1. **情绪 (Sentiment)**: -1.0 到 1.0,表示看空(-)/中性(0)/看多(+) - 2. **确定性 (Confidence)**: 0.0 到 1.0,数值越高越确定 - 3. **强度 (Intensity)**: 1 到 5,数值越高影响越大 - 4. **预期差 (Expectation Gap)**: 0.0 到 1.0,市场预期与现实的差距 - 5. **时效性 (Timeliness)**: 0.0 到 1.0,反应窗口的紧迫程度 - - ### 综合评分算法 - - 综合评分 = 确定性 × 0.35 + 强度/5 × 0.30 + 预期差 × 0.20 + 时效性 × 0.15 - - 范围: 0.0 到 1.0 - - 0.0-0.3: 信号质量较差,不建议跟进 - - 0.3-0.6: 信号质量一般,可作参考 - - 0.6-0.8: 信号质量良好,值得跟进 - - 0.8-1.0: 信号质量优异,强烈推荐 - - ### 评分时的注意事项 - - - **不要混淆方向和强度**:情绪可以是看空,但确定性和强度仍可能很高 - - **预期差往往是 Alpha 来源**:高预期差 + 高确定性 = 最佳博弈机会 - - **考虑时间成本**:长期信号需要更高的确定性才值得跟进 - - **数据为王**:所有评分必须有具体数据支撑 - """, - - applicable_scenarios=[ - "上市公司基本面变化分析", - "产业政策与监管事件评估", - "地缘政治与宏观经济影响", - "技术进步与产业升级", - "突发事件与应急响应" - ], - - aggregation_method="weighted_average", - dimension_weights={ - "confidence": 0.35, - "intensity": 0.30, - "expectation_gap": 0.20, - "timeliness": 0.15 - } -) - - -# ===================================================== -# ISQ Template 管理系统 -# ===================================================== - -class ISQTemplateManager: - """ISQ Template 管理器""" - - def __init__(self): - self.templates: Dict[str, ISQTemplate] = { - DEFAULT_ISQ_TEMPLATE.template_id: DEFAULT_ISQ_TEMPLATE - } - - def register_template(self, template: ISQTemplate) -> None: - """注册新的 template""" - self.templates[template.template_id] = template - - def register_template_dict(self, template_dict: Dict[str, Any]) -> ISQTemplate: - """从 dict 注册模板,返回实例。""" - tpl = ISQTemplate(**template_dict) - self.register_template(tpl) - return tpl - - def get_template(self, template_id: str) -> ISQTemplate: - """获取指定 template""" - if template_id not in self.templates: - return DEFAULT_ISQ_TEMPLATE - return self.templates[template_id] - - def list_templates(self) -> List[Dict[str, str]]: - """列出所有可用 template""" - return [ - { - "id": t.template_id, - "name": t.template_name, - "description": t.description, - "dimensions": list(t.dimensions.keys()) - } - for t in self.templates.values() - ] - - def get_dimension(self, template_id: str, dimension_key: str) -> ISQDimensionSpec: - """获取指定 template 的某个维度定义""" - template = self.get_template(template_id) - return template.dimensions.get(dimension_key) - - def get_scoring_prompt(self, template_id: str) -> str: - """获取用于 LLM 的评分 prompt""" - template = self.get_template(template_id) - - dimensions_desc = "\n".join([ - f"- **{d.name} ({d.key})**\n" - f" 范围: {d.range_type}\n" - f" 说明: {d.description}\n" - f" 示例: {', '.join(f'{k}={v}' for k, v in list(d.examples.items())[:3])}" - for d in template.dimensions.values() - ]) - - return f""" -### ISQ 评估指导 ({template.template_name}) - -使用以下 {len(template.dimensions)} 个维度评估信号质量: - -{dimensions_desc} - -### 评分标准 -{template.scoring_guide} - -### 输出格式 (JSON) -请输出以下 JSON 格式的评分结果: -{{ - "sentiment": , - "confidence": , - "intensity": , - "expectation_gap": , - "timeliness": , - "rationale": {{ - "sentiment": "评分理由", - "confidence": "评分理由", - "intensity": "评分理由", - "expectation_gap": "评分理由", - "timeliness": "评分理由" - }} -}} -""" - - -# 全局 template 管理器实例 -isq_template_manager = ISQTemplateManager() - - -# ===================================================== -# 配置加载 -# ===================================================== - -def load_templates_from_config(config_path: Optional[str] = None) -> None: - """从配置目录加载所有 JSON 模板文件,未找到则跳过,不影响默认模板。 - 支持单个 JSON 文件或目录(目录下的所有 .json 文件)。 - """ - if config_path: - path = Path(config_path) - else: - # 默认目录:config/isq_templates/ - # __file__ = src/schema/isq_template.py - # parent = src/schema, parent.parent = src, parent.parent.parent = 项目根目录 - path = Path(__file__).resolve().parent.parent.parent / "config" - - if not path.exists(): - return - - # 如果是目录,扫描所有 .json 文件 - if path.is_dir(): - json_files = list(path.glob("*.json")) - else: - json_files = [path] - - for json_file in json_files: - try: - data = json.loads(json_file.read_text(encoding="utf-8")) - - # 如果是单个模板对象,转为列表 - if isinstance(data, dict): - templates = [data] - elif isinstance(data, list): - templates = data - else: - continue - - # 注册所有模板 - for tpl_dict in templates: - if not isinstance(tpl_dict, dict): - continue - try: - isq_template_manager.register_template_dict(tpl_dict) - except Exception: - # 忽略单个模板的加载错误,继续其他模板 - continue - except Exception: - # JSON 解析失败,跳过该文件 - continue - - -# 在模块加载时自动尝试加载配置模板 -load_templates_from_config() - - -# ===================================================== -# 便利函数 -# ===================================================== - -def get_isq_template(template_id: str = "default_isq_v1") -> ISQTemplate: - """获取 ISQ template""" - return isq_template_manager.get_template(template_id) - - -def get_isq_scoring_prompt(template_id: str = "default_isq_v1") -> str: - """获取用于 LLM 的 ISQ 评分 prompt""" - return isq_template_manager.get_scoring_prompt(template_id) - - -def calculate_isq_overall_score(scores: Dict[str, float], template_id: str = "default_isq_v1") -> float: - """计算 ISQ 综合评分""" - template = get_isq_template(template_id) - - overall = 0.0 - for dim_key, weight in template.dimension_weights.items(): - if dim_key in scores: - score = scores[dim_key] - # 处理强度维度的特殊缩放 (1-5 -> 0-1) - if dim_key == "intensity": - score = score / 5.0 - overall += score * weight - - return min(1.0, max(0.0, overall)) # 限制在 0-1 之间 diff --git a/skills/alphaear-signal-tracker/scripts/schema/models.py b/skills/alphaear-signal-tracker/scripts/schema/models.py deleted file mode 100644 index 422ca9c..0000000 --- a/skills/alphaear-signal-tracker/scripts/schema/models.py +++ /dev/null @@ -1,100 +0,0 @@ -from pydantic import BaseModel, Field -from typing import List, Optional, Dict, Any -from datetime import datetime - -class TransmissionNode(BaseModel): - node_name: str = Field(..., description="产业链节点名称") - impact_type: str = Field(..., description="利好/利空/中性") - logic: str = Field(..., description="该节点的传导逻辑") - -class IntentAnalysis(BaseModel): - keywords: List[str] = Field(..., description="核心实体、事件或概念关键词") - search_queries: List[str] = Field(..., description="优化后的搜索引擎查询词") - is_specific_event: bool = Field(..., description="是否查询特定突发事件") - time_range: str = Field(..., description="时间范围 (recent/all/specific_date)") - intent_summary: str = Field(..., description="一句话意图描述") - -class FilterResult(BaseModel): - """LLM 筛选结果 - 快速判断是否有有效信号""" - has_valid_signals: bool = Field(..., description="列表中是否包含有效的金融信号") - selected_ids: List[int] = Field(default_factory=list, description="筛选出的有效信号 ID 列表") - themes: List[str] = Field(default_factory=list, description="信号涉及的主题") - reason: Optional[str] = Field(default=None, description="如果无有效信号,说明原因") - -class InvestmentSignal(BaseModel): - # 核心元数据 - signal_id: str = Field(default="unknown_sig", description="唯一信号 ID") - title: str = Field(..., description="信号标题") - summary: str = Field(default="暂无摘要分析", description="100 字核心观点快报") - reasoning: str = Field(default="", description="详细的推演逻辑和理由") - - # 逻辑传导 (ISQ Key 1) - transmission_chain: List[TransmissionNode] = Field(default_factory=list, description="产业链传导逻辑链条") - - # 信号质量 (ISQ Key 2) - 来自 isq_template.DEFAULT_ISQ_TEMPLATE - # 参考: src/schema/isq_template.py 的 DEFAULT_ISQ_TEMPLATE 定义 - sentiment_score: float = Field(default=0.0, description="[ISQ] 情绪/走势 (-1.0=极度看空 ~ 0.0=中性 ~ 1.0=极度看多)") - confidence: float = Field(default=0.5, description="[ISQ] 确定性 (0.0=不可信 ~ 1.0=完全确定)") - intensity: int = Field(default=3, description="[ISQ] 强度/影响量级 (1=微弱 ~ 5=极强)") - expectation_gap: float = Field(default=0.5, description="[ISQ] 预期差/博弈空间 (0.0=充分定价 ~ 1.0=巨大预期差)") - timeliness: float = Field(default=0.8, description="[ISQ] 时效性 (0.0=长期 ~ 1.0=超短期)") - - # 预测与博弈 (ISQ Key 3) - expected_horizon: str = Field(default="T+N", description="预期的反应时窗 (如: T+0, T+3, Long-term)") - price_in_status: str = Field(default="未知", description="市场预期消化程度 (未定价/部分定价/充分定价)") - - # 关联实体 - impact_tickers: List[Dict[str, Any]] = Field(default_factory=list, description="受影响的代码列表及其权重") - industry_tags: List[str] = Field(default_factory=list, description="关联行业标签") - - # 溯源 - sources: List[Dict[str, str]] = Field(default_factory=list, description="来源详情 (包含 title, url, source_name)") - -class ResearchContext(BaseModel): - """研究员搜集的背景信息结构""" - raw_signal: str = Field(..., description="原始信号内容") - tickers_found: List[Dict[str, Any]] = Field(default_factory=list, description="找到的相关标的及其基本面/股价信息") - industry_background: str = Field(..., description="行业背景及产业链现状") - latest_developments: List[str] = Field(default_factory=list, description="相关事件的最新进展") - key_risks: List[str] = Field(default_factory=list, description="潜在风险点") - search_results_summary: str = Field(..., description="搜索结果的综合摘要") - -class ScanContext(BaseModel): - """扫描员搜集的原始数据结构""" - hot_topics: List[str] = Field(..., description="当前市场热点话题") - news_summaries: List[Dict[str, Any]] = Field(..., description="关键新闻摘要列表") - market_data: Dict[str, Any] = Field(default_factory=dict, description="相关的市场行情数据") - sentiment_overview: str = Field(..., description="整体市场情绪概览") - raw_data_summary: str = Field(..., description="原始数据的综合摘要") - -class SignalCluster(BaseModel): - theme_title: str = Field(..., description="主题名称") - signal_ids: List[int] = Field(..., description="包含的信号 ID 列表") - rationale: str = Field(..., description="聚类理由") - -class ClusterContext(BaseModel): - """信号聚类结果结构""" - clusters: List[SignalCluster] = Field(..., description="聚类列表") - -class KLinePoint(BaseModel): - date: str = Field(..., description="日期") - open: float = Field(..., description="开盘价") - high: float = Field(..., description="最高价") - low: float = Field(..., description="最低价") - close: float = Field(..., description="收盘价") - volume: float = Field(..., description="成交量") - -class ForecastResult(BaseModel): - ticker: str = Field(..., description="股票代码") - base_forecast: List[KLinePoint] = Field(default_factory=list, description="Kronos 模型原始预测") - adjusted_forecast: List[KLinePoint] = Field(default_factory=list, description="LLM 调整后的预测") - rationale: str = Field(default="", description="预测调整理由及逻辑说明") - timestamp: str = Field(default_factory=lambda: datetime.now().strftime("%Y-%m-%d %H:%M:%S"), description="生成时间") - -class InvestmentReport(BaseModel): - overall_sentiment: str = Field(..., description="整体市场情绪评价") - market_entropy: float = Field(..., description="市场分歧度 (0-1, 1代表极高分歧)") - signals: List[InvestmentSignal] = Field(..., description="深度解析的投资信号列表") - forecasts: List[ForecastResult] = Field(default_factory=list, description="相关标的的预测结果") - timestamp: str = Field(..., description="报告生成时间") - meta_info: Optional[Dict[str, Any]] = Field(default_factory=dict, description="其他元数据") diff --git a/skills/alphaear-signal-tracker/scripts/tools/__init__.py b/skills/alphaear-signal-tracker/scripts/tools/__init__.py deleted file mode 100644 index 97fbb5d..0000000 --- a/skills/alphaear-signal-tracker/scripts/tools/__init__.py +++ /dev/null @@ -1,24 +0,0 @@ -# src/tools/__init__.py -""" -AlphaEar 工具包层 - Agno Toolkit 适配器 - -提供的 Toolkit 类: -- NewsToolkit: 热点新闻获取 -- StockToolkit: 股票搜索与价格查询 -- SentimentToolkit: 情绪分析 -- SearchToolkit: 网络搜索 -""" - -from .toolkits import ( - NewsToolkit, - StockToolkit, - SentimentToolkit, - SearchToolkit, -) - -__all__ = [ - "NewsToolkit", - "StockToolkit", - "SentimentToolkit", - "SearchToolkit", -] diff --git a/skills/alphaear-signal-tracker/scripts/tools/toolkits.py b/skills/alphaear-signal-tracker/scripts/tools/toolkits.py deleted file mode 100644 index ebd0b69..0000000 --- a/skills/alphaear-signal-tracker/scripts/tools/toolkits.py +++ /dev/null @@ -1,526 +0,0 @@ -""" -AlphaEar 工具包层 - Agno Toolkit 适配器 -复用 utils 中的底层工具实现,提供 Agno Agent 兼容的 Toolkit 接口 -""" -from datetime import datetime -from typing import Optional -from agno.tools import Toolkit -from loguru import logger - -from ..utils.database_manager import DatabaseManager -from ..utils.news_tools import NewsNowTools, PolymarketTools -from ..utils.stock_tools import StockTools -from ..utils.search_tools import SearchTools -from ..utils.sentiment_tools import SentimentTools - - -class NewsToolkit(Toolkit): - """ - 新闻工具包 - 包装 NewsNowTools 为 Agno Toolkit - - 提供热点新闻获取、内容提取等功能 - """ - - def __init__(self, db: DatabaseManager, **kwargs): - self._news_tools = NewsNowTools(db) - self._sources = self._news_tools.SOURCES - - tools = [ - self.fetch_hot_news, - self.fetch_news_content, - self.get_unified_trends, - self.enrich_news_content, - ] - super().__init__(name="news_toolkit", tools=tools, **kwargs) - - - def fetch_hot_news(self, source_id: str, count: int = 10) -> str: - """ - 从指定新闻源获取热点新闻列表。 - - Args: - source_id: 新闻源标识符。可选值按类别: - **金融类**: "cls" (财联社), "wallstreetcn" (华尔街见闻), "xueqiu" (雪球) - **综合类**: "weibo" (微博热搜), "zhihu" (知乎热榜), "baidu" (百度热搜), - "toutiao" (今日头条), "douyin" (抖音), "thepaper" (澎湃新闻) - **科技类**: "36kr" (36氪), "ithome" (IT之家), "v2ex", "juejin" (掘金), - "hackernews" (Hacker News) - 推荐金融分析使用 "cls", "wallstreetcn", "xueqiu"。 - count: 获取的新闻数量,默认 10 条。 - - Returns: - 热点新闻列表的文本描述,包含排名、标题和链接。如果源不可用则返回错误信息。 - """ - logger.info(f"🔧 [TOOL CALLED] fetch_hot_news(source_id={source_id}, count={count})") - - items = self._news_tools.fetch_hot_news(source_id, count=count, fetch_content=False) - - if not items: - return f"获取 {source_id} 热点失败" - - source_name = self._sources.get(source_id, source_id) - result = f"## {source_name} 热点 (获取时间: {datetime.now().strftime('%Y-%m-%d %H:%M')})\n\n" - - for item in items: - result += f"{item['rank']}. {item['title']}\n 链接: {item['url']}\n\n" - - logger.info(f"✅ [TOOL SUCCESS] Got {len(items)} news items from {source_id}") - return result - - def fetch_news_content(self, url: str) -> str: - """ - 使用 Jina Reader 抓取指定 URL 的网页正文内容。 - - Args: - url: 需要抓取内容的完整网页 URL,必须以 http:// 或 https:// 开头。 - - Returns: - 提取的网页正文内容,如果失败则返回错误信息。 - """ - content = self._news_tools.fetch_news_content(url) - if content: - return content[:5000] # 限制长度 - return "内容抓取失败" - - def get_unified_trends(self, sources: str = "wallstreetcn,cls") -> str: - """ - 获取多平台综合热点报告。 - - Args: - sources: 要扫描的新闻源,用逗号分隔。 - 可选值: weibo, zhihu, baidu, toutiao, wallstreetcn, cls - 默认: "wallstreetcn,cls" (金融资讯) - - Returns: - 格式化的热点汇总报告。 - """ - source_list = [s.strip() for s in sources.split(",")] - report = self._news_tools.get_unified_trends(source_list) - return report - - def enrich_news_content(self, source: str = None, limit: int = 5) -> str: - """ - 为数据库中缺少正文内容的新闻补充内容。 - - Args: - source: 筛选特定新闻源(如 "cls"),为空则处理所有。 - limit: 最多处理的新闻数量,默认 5 条。 - - Returns: - 处理结果的描述。 - """ - logger.info(f"🔧 [TOOL CALLED] enrich_news_content(source={source}, limit={limit})") - - # 获取需要补充内容的新闻 - news_items = self._news_tools.db.get_daily_news(source=source, limit=limit) - items_without_content = [n for n in news_items if not n.get('content')] - - if not items_without_content: - return "没有需要补充内容的新闻" - - updated_count = 0 - cursor = self._news_tools.db.conn.cursor() - - for item in items_without_content[:limit]: - url = item.get('url') - if url: - content = self._news_tools.fetch_news_content(url) - if content: - cursor.execute( - "UPDATE daily_news SET content = ? WHERE id = ?", - (content[:10000], item['id']) - ) - updated_count += 1 - - self._news_tools.db.conn.commit() - logger.info(f"✅ [TOOL SUCCESS] Enriched {updated_count} news items with content") - - return f"✅ 已为 {updated_count} 条新闻补充正文内容" - - -class PolymarketToolkit(Toolkit): - """ - Polymarket 预测市场工具包 - 获取热门预测市场数据 - - 预测市场数据可反映公众情绪、预期和关注度 - """ - - def __init__(self, db: DatabaseManager, **kwargs): - self._poly_tools = PolymarketTools(db) - - tools = [ - self.get_prediction_markets, - self.get_market_summary, - ] - super().__init__(name="polymarket_toolkit", tools=tools, **kwargs) - - def get_prediction_markets(self, limit: int = 20) -> str: - """ - 获取 Polymarket 活跃预测市场的关键数据。 - - 预测市场反映公众对重大事件的概率预期,可用于: - - 分析市场情绪和风险偏好 - - 了解热门话题的关注度 - - 获取重大事件的概率预期 - - Args: - limit: 获取的市场数量,默认 20 个。 - - Returns: - 预测市场数据列表,包含问题、结果概率和交易量。 - 如果获取失败返回错误信息。 - """ - logger.info(f"🔧 [TOOL CALLED] get_prediction_markets(limit={limit})") - - markets = self._poly_tools.get_active_markets(limit) - if not markets: - return "❌ 无法获取 Polymarket 数据(可能是网络问题)" - - result = f"## 🔮 Polymarket 热门预测 (共 {len(markets)} 个)\n\n" - for i, m in enumerate(markets[:limit], 1): - question = m.get("question", "Unknown") - prices = m.get("outcomePrices", []) - volume = m.get("volume", 0) - - result += f"{i}. **{question}**\n" - if prices: - result += f" 概率: {prices}\n" - if volume: - try: - result += f" 交易量: ${float(volume):,.0f}\n" - except: - result += f" 交易量: {volume}\n" - result += "\n" - - logger.info(f"✅ [TOOL SUCCESS] Got {len(markets)} prediction markets") - return result - - def get_market_summary(self, limit: int = 10) -> str: - """ - 获取预测市场摘要报告,了解当前热门话题和公众预期。 - - Args: - limit: 获取的市场数量,默认 10 个。 - - Returns: - 格式化的预测市场报告。 - """ - return self._poly_tools.get_market_summary(limit) - - -class StockToolkit(Toolkit): - - """ - 股票工具包 - 包装 StockTools 为 Agno Toolkit - - 提供股票搜索、价格查询等功能 - """ - - def __init__(self, db: DatabaseManager, **kwargs): - self._stock_tools = StockTools(db) - - tools = [ - self.search_ticker, - self.get_stock_price, - ] - super().__init__(name="stock_toolkit", tools=tools, **kwargs) - - def search_ticker(self, query: str) -> str: - """ - 模糊搜索 A 股股票代码或名称。 - - Args: - query: 搜索关键词,可以是股票代码(如 "600519")或名称关键词(如 "茅台"、"宁德"、"比亚迪")。 - - Returns: - 匹配的股票列表,包含代码和名称。 - """ - q = (query or "").strip() - # Guardrails: prevent overly generic queries that tend to return arbitrary "...股份" matches. - generic_terms = { - "股份", - "有限公司", - "概念股", - "受益股", - "龙头", - "标的", - "相关股票", - "合作概念股", - } - if not q: - return "查询为空,无法搜索股票" - if q in generic_terms: - return f"查询过于泛化({q}),为避免误匹配已拒绝。请提供更具体的公司名或6位代码。" - # If it's not a numeric code, require at least 2 non-space chars. - if not any(ch.isdigit() for ch in q) and len(q.replace(" ", "")) < 2: - return "查询过短,无法搜索股票。请提供更具体的公司名或6位代码。" - - results = self._stock_tools.search_ticker(query) - - if not results: - return f"未找到匹配 '{query}' 的股票" - - output = f"## 股票搜索结果 (关键词: {query})\n\n" - for r in results: - output += f"- {r['code']} - {r['name']}\n" - return output - - def get_stock_price(self, ticker: str, days: int = 30) -> str: - """ - 获取指定股票的近期价格走势。 - - Args: - ticker: 股票代码,如 "600519"(贵州茅台)或 "000001"(平安银行)。 - days: 查询天数,默认 30 天。 - - Returns: - 价格走势的文本摘要。 - """ - from datetime import timedelta - end_date = datetime.now().strftime('%Y-%m-%d') - start_date = (datetime.now() - timedelta(days=days)).strftime('%Y-%m-%d') - - df = self._stock_tools.get_stock_price(ticker, start_date, end_date) - - if df.empty: - return f"未能获取 {ticker} 的股价数据" - - - latest = df.iloc[-1] - change = ((latest['close'] - df.iloc[0]['close']) / df.iloc[0]['close']) * 100 - - # 格式化历史数据供 LLM 分析 (取最近 15 天) - history_df = df.tail(15).copy() - history_df['date'] = history_df['date'].astype(str) - # 简化列名以节省 token - history_cols = ['date', 'open', 'close', 'high', 'low', 'volume'] - - # 尝试使用 markdown 格式,如果失败退回到 string - try: - history_str = history_df[history_cols].to_markdown(index=False, numalign="left", stralign="left") - except ImportError: - history_str = history_df[history_cols].to_string(index=False) - except Exception: - history_str = history_df[history_cols].to_string(index=False) - - return f"""## {ticker} 价格走势 ({days}天) -- 当前价: ¥{latest['close']:.2f} -- 期间涨跌: {change:+.2f}% -- 最高/最低: ¥{df['high'].max():.2f} / ¥{df['low'].min():.2f} -- 数据范围: {df.iloc[0]['date']} -> {latest['date']} - -### 最近 15 个交易日详细数据 (OHLCV): -{history_str} -""" - - - -class SentimentToolkit(Toolkit): - """ - 情绪分析工具包 - 包装 SentimentTools 为 Agno Toolkit - - 提供文本情绪分析功能(支持 BERT 和 LLM 模式) - """ - - def __init__(self, db: DatabaseManager, mode: str = "auto", **kwargs): - self._sentiment_tools = SentimentTools(db, mode=mode) - self._db = db - - tools = [ - self.analyze_sentiment, - self.batch_update_sentiment, - ] - super().__init__(name="sentiment_toolkit", tools=tools, **kwargs) - - def analyze_sentiment(self, text: str) -> str: - """ - 分析文本的情绪极性。 - - Args: - text: 需要分析的文本内容,如新闻标题或摘要。 - - Returns: - 情绪分析结果,包含分值(-1.0到1.0)和标签(positive/negative/neutral)。 - """ - result = self._sentiment_tools.analyze_sentiment(text) - - score = result.get('score', 0.0) - label = result.get('label', 'neutral') - reason = result.get('reason', '') - - return f"""情绪分析结果: -- 文本: {text[:100]}{'...' if len(text) > 100 else ''} -- 分值: {score:.2f} -- 标签: {label} -- 分析: {reason}""" - - def batch_update_sentiment(self, source: str = None, limit: int = 20) -> str: - """ - 批量更新数据库中新闻的情绪分数。 - - Args: - source: 筛选特定新闻源(如 "cls", "wallstreetcn"),为空则处理所有。 - limit: 最多处理的新闻数量,默认 20 条。 - - Returns: - 更新结果的描述。 - """ - logger.info(f"🔧 [TOOL CALLED] batch_update_sentiment(source={source}, limit={limit})") - - count = self._sentiment_tools.batch_update_news_sentiment(source=source, limit=limit) - - return f"✅ 已更新 {count} 条新闻的情绪分数" - - - -class SearchToolkit(Toolkit): - """ - 搜索工具包 - 包装 SearchTools 为 Agno Toolkit - - 提供网络搜索功能(支持 Jina、DuckDuckGo 和百度) - - 当环境变量 JINA_API_KEY 设置时,默认使用 Jina Search, - 提供 LLM 友好的搜索结果。 - """ - - def __init__(self, db: DatabaseManager, **kwargs): - self._search_tools = SearchTools(db) - - tools = [ - self.web_search, - self.aggregate_search, - ] - super().__init__(name="search_toolkit", tools=tools, **kwargs) - - def web_search(self, query: str, engine: str = None, max_results: int = 5) -> str: - """ - 使用指定搜索引擎执行网络搜索。 - - Args: - query: 搜索关键词,如 "英伟达财报" 或 "光伏行业政策"。 - engine: 搜索引擎选择。可选值: - "jina" (Jina Search,需配置 JINA_API_KEY,LLM友好输出), - "ddg" (DuckDuckGo,推荐英文/国际搜索), - "baidu" (百度,推荐中文/国内搜索)。 - 默认: 若配置了 JINA_API_KEY 则使用 "jina",否则 "ddg"。 - max_results: 返回结果数量。默认 5。 - - Returns: - 搜索结果的文本描述。 - """ - return self._search_tools.search(query, engine=engine, max_results=max_results) - - def aggregate_search(self, query: str, max_results: int = 5) -> str: - """ - 同时使用多个搜索引擎搜索并聚合结果。 - - Args: - query: 搜索关键词。 - max_results: 每个引擎返回的最大结果数。默认 5。 - - Returns: - 聚合后的搜索结果。 - """ - return self._search_tools.aggregate_search(query, max_results=max_results) - - -class ContextSearchToolkit(Toolkit): - """ - 上下文搜索工具包 - 用于 RAG 场景的文档片段检索 - - 支持在内存中存储文档片段,并通过关键词搜索相关内容。 - 适用于 ReportAgent 的分段编辑场景。 - """ - - def __init__(self, **kwargs): - self._store = {} # {doc_id: {"title": str, "content": str, "summary": str}} - - tools = [ - self.search_context, - self.get_toc, - ] - super().__init__(name="context_search_toolkit", tools=tools, **kwargs) - - def add_document(self, doc_id: str, title: str, content: str, summary: str = ""): - """添加文档到存储(供外部调用,非 LLM 工具)""" - self._store[doc_id] = { - "title": title, - "content": content, - "summary": summary or content[:200] + "..." - } - logger.info(f"📄 Added document to context store: {doc_id} - {title[:30]}...") - - def clear(self): - """清空文档存储""" - self._store.clear() - logger.info("🗑️ Context store cleared") - - def search_context(self, query: str, max_results: int = 3) -> str: - """ - 在已存储的文档中搜索与查询相关的内容片段。 - - Args: - query: 搜索关键词,如 "消费板块" 或 "茅台 预测"。 - max_results: 返回的最大结果数,默认 3。 - - Returns: - 匹配的文档片段,按相关性排序。 - """ - logger.info(f"🔍 [TOOL CALLED] search_context(query={query}, max_results={max_results})") - - if not self._store: - return "⚠️ 上下文存储为空,无可搜索内容。" - - # 简单的关键词匹配 + 计分 - query_terms = query.lower().split() - results = [] - - for doc_id, doc in self._store.items(): - score = 0 - content_lower = doc["content"].lower() - title_lower = doc["title"].lower() - - for term in query_terms: - # 标题匹配权重更高 - if term in title_lower: - score += 3 - if term in content_lower: - score += content_lower.count(term) - - if score > 0: - results.append((score, doc_id, doc)) - - # 按分数排序 - results.sort(key=lambda x: x[0], reverse=True) - results = results[:max_results] - - if not results: - return f"未找到与 '{query}' 相关的内容。" - - output = f"## 搜索结果 (查询: {query})\n\n" - for score, doc_id, doc in results: - output += f"### [{doc_id}] {doc['title']}\n" - # 返回摘要而非全文,节省 token - output += f"{doc['summary']}\n\n" - - logger.info(f"✅ [TOOL SUCCESS] Found {len(results)} matching documents") - return output - - def get_toc(self) -> str: - """ - 获取当前存储的所有文档的目录(TOC)。 - - Returns: - 文档目录列表,包含 ID 和标题。 - """ - logger.info("🔍 [TOOL CALLED] get_toc()") - - if not self._store: - return "⚠️ 上下文存储为空。" - - output = "## 文档目录 (TOC)\n\n" - for doc_id, doc in self._store.items(): - output += f"- **[{doc_id}]** {doc['title']}\n" - - return output - diff --git a/skills/alphaear-signal-tracker/scripts/utils/__init__.py b/skills/alphaear-signal-tracker/scripts/utils/__init__.py deleted file mode 100644 index 27e1961..0000000 --- a/skills/alphaear-signal-tracker/scripts/utils/__init__.py +++ /dev/null @@ -1 +0,0 @@ -# AlphaEar utils package diff --git a/skills/alphaear-signal-tracker/scripts/utils/content_extractor.py b/skills/alphaear-signal-tracker/scripts/utils/content_extractor.py deleted file mode 100644 index 133207a..0000000 --- a/skills/alphaear-signal-tracker/scripts/utils/content_extractor.py +++ /dev/null @@ -1,122 +0,0 @@ -import requests -from requests.exceptions import RequestException, Timeout, ConnectionError -import os -import time -import json -import threading -from typing import Optional -from loguru import logger - - -class ContentExtractor: - """内容提取工具 - 主要接入 Jina Reader API""" - - JINA_BASE_URL = "https://r.jina.ai/" - - # 速率限制配置 (无 API Key 时:20 次/分钟) - _rate_limit_no_key = 20 # 每分钟最大请求数 - _rate_window = 60.0 # 时间窗口(秒) - _min_interval = 3.0 # 请求最小间隔(秒) - - # 类级别的速率限制状态 - _request_times = [] - _last_request_time = 0.0 - _lock = threading.Lock() - - @classmethod - def _wait_for_rate_limit(cls, has_api_key: bool) -> None: - """等待以满足速率限制要求""" - if has_api_key: - # 有 API Key 时,只需保持最小间隔 - time.sleep(0.5) - return - - with cls._lock: - current_time = time.time() - - # 1. 清理过期的请求记录 - cls._request_times = [t for t in cls._request_times if current_time - t < cls._rate_window] - - # 2. 检查是否达到速率限制 - if len(cls._request_times) >= cls._rate_limit_no_key: - # 需要等待最旧的请求过期 - oldest = cls._request_times[0] - wait_time = cls._rate_window - (current_time - oldest) + 1.0 - if wait_time > 0: - logger.warning(f"⏳ Jina rate limit reached, waiting {wait_time:.1f}s...") - time.sleep(wait_time) - current_time = time.time() - cls._request_times = [t for t in cls._request_times if current_time - t < cls._rate_window] - - # 3. 确保请求间隔不太快 - time_since_last = current_time - cls._last_request_time - if time_since_last < cls._min_interval: - sleep_time = cls._min_interval - time_since_last - time.sleep(sleep_time) - - # 4. 记录本次请求 - cls._request_times.append(time.time()) - cls._last_request_time = time.time() - - @classmethod - def extract_with_jina(cls, url: str, timeout: int = 30) -> Optional[str]: - """ - 使用 Jina Reader 提取网页正文内容 (Markdown 格式) - - 无 API Key 时自动限速:每分钟最多 20 次请求,每次间隔至少 3 秒 - """ - if not url or not url.startswith("http"): - return None - - logger.info(f"🕸️ Extracting content from: {url} via Jina...") - - headers = { - "User-Agent": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/131.0.0.0 Safari/537.36", - "Accept": "application/json" - } - - # 使用统一的 JINA_API_KEY - api_key = os.getenv("JINA_API_KEY") - has_api_key = bool(api_key and api_key.strip()) - - if has_api_key: - headers["Authorization"] = f"Bearer {api_key}" - - # 等待速率限制 - cls._wait_for_rate_limit(has_api_key) - - try: - # Jina Reader API - full_url = f"{cls.JINA_BASE_URL}{url}" - response = requests.get(full_url, headers=headers, timeout=timeout) - - if response.status_code == 200: - try: - data = response.json() - # Jina JSON 响应格式通常在 data.content - if isinstance(data, dict) and "data" in data: - return data["data"].get("content", "") - return data.get("content", response.text) - except (json.JSONDecodeError, TypeError): - return response.text - elif response.status_code == 429: - # 触发速率限制,等待后重试一次 - logger.warning(f"⚠️ Jina rate limit (429), waiting 60s before retry...") - time.sleep(60) - return cls.extract_with_jina(url, timeout) - else: - logger.warning(f"Jina extraction failed (Status {response.status_code}) for {url}") - return None - - except Timeout: - logger.error(f"Timeout during Jina extraction for {url}") - return None - except ConnectionError: - logger.error(f"Connection error during Jina extraction for {url}") - return None - except RequestException as e: - logger.error(f"Request error during Jina extraction: {e}") - return None - except Exception as e: - logger.error(f"Unexpected error during Jina extraction: {e}") - return None diff --git a/skills/alphaear-signal-tracker/scripts/utils/database_manager.py b/skills/alphaear-signal-tracker/scripts/utils/database_manager.py deleted file mode 100644 index cfc362b..0000000 --- a/skills/alphaear-signal-tracker/scripts/utils/database_manager.py +++ /dev/null @@ -1,581 +0,0 @@ -import sqlite3 -import json -from datetime import datetime, date -from pathlib import Path -from typing import List, Dict, Optional, Any, Union -import pandas as pd -from loguru import logger - -class DatabaseManager: - """ - AlphaEar 数据库管理器 - 负责存储热点数据、搜索缓存和股价数据 - 使用 SQLite 进行持久化存储 - """ - - def __init__(self, db_path: str = "data/signal_flux.db"): - self.db_path = Path(db_path) - self.db_path.parent.mkdir(parents=True, exist_ok=True) - self.conn = sqlite3.connect(str(self.db_path), check_same_thread=False) - self.conn.row_factory = sqlite3.Row - self._init_db() - logger.info(f"💾 Database initialized at {self.db_path}") - - def _init_db(self): - """初始化表结构""" - cursor = self.conn.cursor() - - # 1. 每日热点新闻表 - cursor.execute(""" - CREATE TABLE IF NOT EXISTS daily_news ( - id TEXT PRIMARY KEY, - source TEXT, - rank INTEGER, - title TEXT, - url TEXT, - content TEXT, - publish_time TEXT, - crawl_time TEXT, - sentiment_score REAL, - analysis TEXT, - meta_data TEXT - ) - """) - - # 尝试添加 analysis 列(如果表已存在但没有该列) - try: - cursor.execute("ALTER TABLE daily_news ADD COLUMN analysis TEXT") - except: - pass # 列已存在 - - - # 2. 搜索缓存表 (原有 JSON 缓存) - cursor.execute(""" - CREATE TABLE IF NOT EXISTS search_cache ( - query_hash TEXT PRIMARY KEY, - query TEXT, - engine TEXT, - results TEXT, - timestamp TEXT - ) - """) - - # 2.5 搜索详情表 (展开的搜索结果) - cursor.execute(""" - CREATE TABLE IF NOT EXISTS search_detail ( - id TEXT, - query_hash TEXT, - rank INTEGER, - title TEXT, - url TEXT, - content TEXT, - publish_time TEXT, - crawl_time TEXT, - sentiment_score REAL, - source TEXT, - meta_data TEXT, - PRIMARY KEY (query_hash, id) - ) - """) - - # 3. 股价数据表 - cursor.execute(""" - CREATE TABLE IF NOT EXISTS stock_prices ( - ticker TEXT, - date TEXT, - open REAL, - close REAL, - high REAL, - low REAL, - volume REAL, - change_pct REAL, - PRIMARY KEY (ticker, date) - ) - """) - - # 4. 股票列表表 (用于检索) - cursor.execute(""" - CREATE TABLE IF NOT EXISTS stock_list ( - code TEXT PRIMARY KEY, - name TEXT - ) - """) - - # 5. 投资信号表 (ISQ Framework) - cursor.execute(""" - CREATE TABLE IF NOT EXISTS signals ( - signal_id TEXT PRIMARY KEY, - title TEXT, - summary TEXT, - transmission_chain TEXT, - sentiment_score REAL, - confidence REAL, - intensity INTEGER, - expected_horizon TEXT, - price_in_status TEXT, - impact_tickers TEXT, - industry_tags TEXT, - sources TEXT, - user_id TEXT, - created_at TEXT - ) - """) - - - - # 6. 创建索引以优化查询性能 - cursor.execute("CREATE INDEX IF NOT EXISTS idx_news_crawl_time ON daily_news(crawl_time)") - cursor.execute("CREATE INDEX IF NOT EXISTS idx_news_source ON daily_news(source)") - cursor.execute("CREATE INDEX IF NOT EXISTS idx_search_cache_timestamp ON search_cache(timestamp)") - cursor.execute("CREATE INDEX IF NOT EXISTS idx_stock_prices_ticker_date ON stock_prices(ticker, date)") - # 尝试添加 user_id 列到 signals 表 - try: - cursor.execute("ALTER TABLE signals ADD COLUMN user_id TEXT") - except: - pass - - cursor.execute("CREATE INDEX IF NOT EXISTS idx_signals_user_id ON signals(user_id)") - - self.conn.commit() - - # - # self.conn.commit() - - - # --- 新闻数据操作 --- - - def save_daily_news(self, news_list: List[Dict]) -> int: - """保存热点新闻,包含发布时间与抓取时间""" - cursor = self.conn.cursor() - count = 0 - crawl_time = datetime.now().isoformat() - - for news in news_list: - try: - # 兼容不同来源的 ID 生成逻辑 - news_id = news.get('id') or f"{news.get('source')}_{news.get('rank')}_{crawl_time[:10]}" - cursor.execute(""" - INSERT OR REPLACE INTO daily_news - (id, source, rank, title, url, content, publish_time, crawl_time, sentiment_score, meta_data) - VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?) - """, ( - news_id, - news.get('source'), - news.get('rank'), - news.get('title'), - news.get('url'), - news.get('content', ''), - news.get('publish_time'), # 新增支持发布时间 - crawl_time, - news.get('sentiment_score'), - json.dumps(news.get('meta_data', {})) - )) - count += 1 - except sqlite3.Error as e: - logger.error(f"Database error saving news item {news.get('title')}: {e}") - except Exception as e: - logger.error(f"Unexpected error saving news item {news.get('title')}: {e}") - - self.conn.commit() - return count - - def get_daily_news(self, source: Optional[str] = None, limit: int = 100, days: int = 1) -> List[Dict]: - """获取最近 N 天的热点新闻""" - cursor = self.conn.cursor() - # 使用 crawl_time 过滤,保证结果的新鲜度 - time_threshold = (datetime.now().timestamp() - days * 86400) - time_threshold_str = datetime.fromtimestamp(time_threshold).isoformat() - - query = "SELECT * FROM daily_news WHERE crawl_time >= ?" - params = [time_threshold_str] - - if source: - query += " AND source = ?" - params.append(source) - - query += " ORDER BY crawl_time DESC, rank LIMIT ?" - params.append(limit) - - cursor.execute(query, params) - return [dict(row) for row in cursor.fetchall()] - - def lookup_reference_by_url(self, url: str) -> Optional[Dict[str, Any]]: - """Best-effort lookup of a source item by URL. - - This is used to render a stable bibliography from DB-backed metadata. - It searches both `daily_news` and `search_detail`. - """ - url = (url or "").strip() - if not url: - return None - - cursor = self.conn.cursor() - - try: - cursor.execute( - """ - SELECT title, source, publish_time, crawl_time, url - FROM daily_news - WHERE url = ? - ORDER BY crawl_time DESC - LIMIT 1 - """, - (url,), - ) - row = cursor.fetchone() - if row: - return dict(row) - except Exception: - pass - - try: - cursor.execute( - """ - SELECT title, source, publish_time, crawl_time, url - FROM search_detail - WHERE url = ? - ORDER BY crawl_time DESC - LIMIT 1 - """, - (url,), - ) - row = cursor.fetchone() - if row: - return dict(row) - except Exception: - pass - - return None - - def delete_news(self, news_id: str) -> bool: - """删除特定新闻""" - cursor = self.conn.cursor() - cursor.execute("DELETE FROM daily_news WHERE id = ?", (news_id,)) - self.conn.commit() - return cursor.rowcount > 0 - - def update_news_content(self, news_id: str, content: str = None, analysis: str = None) -> bool: - """更新新闻的内容或分析结果""" - cursor = self.conn.cursor() - updates = [] - params = [] - - if content is not None: - updates.append("content = ?") - params.append(content) - if analysis is not None: - updates.append("analysis = ?") - params.append(analysis) - - if not updates: - return False - - params.append(news_id) - query = f"UPDATE daily_news SET {', '.join(updates)} WHERE id = ?" - cursor.execute(query, params) - self.conn.commit() - return cursor.rowcount > 0 - - # --- 搜索缓存辅助 --- - - def get_search_cache(self, query_hash: str, ttl_seconds: Optional[int] = None) -> Optional[Dict]: - """获取搜索缓存 (优先查 search_detail)""" - cursor = self.conn.cursor() - - # 1. 尝试从 search_detail 获取展开的结构化数据 - cursor.execute(""" - SELECT * FROM search_detail - WHERE query_hash = ? - ORDER BY rank - """, (query_hash,)) - details = [dict(row) for row in cursor.fetchall()] - - if details: - # 检查 TTL (取第一条的时间) - first_time = datetime.fromisoformat(details[0]['crawl_time']) - if ttl_seconds and (datetime.now() - first_time).total_seconds() > ttl_seconds: - logger.info(f"⌛ Detailed cache expired for hash {query_hash}") - pass # Expired, fall through or return None? If Detail expired, Cache likely expired too. - # But let's check basic cache just in case metadata differs? - # Actually if details exist, we prefer them. If expired, we return None. - return None - - logger.info(f"✅ Hit detailed search cache for {query_hash} ({len(details)} items)") - # Reconstruct the expected 'results' list format for SearchTools - # SearchTools expects a list of dicts. - # We return a dict wrapper to match get_search_cache signature returning Dict usually containing 'results' string. - # But SearchTools logic: - # cache = db.get_search_cache(...) - # cached_data = json.loads(cache['results']) - - # To minimize SearchTools changes, we can return a dict mimicking the old structure - # OR Change SearchTools to handle list return. - # Let's return a special dict that SearchTools can recognize or just format it as before. - return {"results": json.dumps(details), "timestamp": details[0]['crawl_time']} - - # 2. Fallback to old table - cursor.execute("SELECT * FROM search_cache WHERE query_hash = ?", (query_hash,)) - row = cursor.fetchone() - - if not row: - return None - - row_dict = dict(row) - if ttl_seconds: - cache_time = datetime.fromisoformat(row_dict['timestamp']) - if (datetime.now() - cache_time).total_seconds() > ttl_seconds: - logger.info(f"⌛ Cache expired for hash {query_hash}") - return None - - return row_dict - - def save_search_cache(self, query_hash: str, query: str, engine: str, results: Union[str, List[Dict]]): - """保存搜索结果 (同时保存到 search_cache 和 search_detail)""" - cursor = self.conn.cursor() - current_time = datetime.now().isoformat() - - results_str = results if isinstance(results, str) else json.dumps(results) - - # 1. Save summary to search_cache - cursor.execute(""" - INSERT OR REPLACE INTO search_cache (query_hash, query, engine, results, timestamp) - VALUES (?, ?, ?, ?, ?) - """, (query_hash, query, engine, results_str, current_time)) - - # 2. Save details to search_detail if results is a list - if isinstance(results, list): - for item in results: - try: - item_id = item.get('id') or f"{hash(item.get('url', ''))}" - cursor.execute(""" - INSERT OR REPLACE INTO search_detail - (id, query_hash, rank, title, url, content, publish_time, crawl_time, sentiment_score, source, meta_data) - VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?) - """, ( - str(item_id), - query_hash, - item.get('rank', 0), - item.get('title'), - item.get('url'), - item.get('content', ''), - item.get('publish_time'), - item.get('crawl_time') or current_time, - item.get('sentiment_score'), - item.get('source'), - json.dumps(item.get('meta_data', {})) - )) - except sqlite3.Error as e: - logger.error(f"Database error saving search detail {item.get('title')}: {e}") - except Exception as e: - logger.error(f"Unexpected error saving search detail {item.get('title')}: {e}") - - self.conn.commit() - - def find_similar_queries(self, query: str, limit: int = 5) -> List[Dict]: - """模糊搜索相似的已缓存查询""" - cursor = self.conn.cursor() - - # Simple fuzzy match: query in cached OR cached in query - q_wild = f"%{query}%" - cursor.execute(""" - SELECT query, query_hash, timestamp, results - FROM search_cache - WHERE query LIKE ? OR ? LIKE ('%' || query || '%') - ORDER BY timestamp DESC - LIMIT ? - """, (q_wild, query, limit)) - - return [dict(row) for row in cursor.fetchall()] - - def search_local_news(self, query: str, limit: int = 5) -> List[Dict]: - """从本地 daily_news 搜索相关新闻""" - cursor = self.conn.cursor() - q_wild = f"%{query}%" - # Search title and content - cursor.execute(""" - SELECT * FROM daily_news - WHERE title LIKE ? OR content LIKE ? - ORDER BY crawl_time DESC - LIMIT ? - """, (q_wild, q_wild, limit)) - return [dict(row) for row in cursor.fetchall()] - - # --- 股票数据操作 --- - - def save_stock_list(self, df: pd.DataFrame): - """保存股票列表到 stock_list 表""" - cursor = self.conn.cursor() - try: - # 清空旧表 - cursor.execute("DELETE FROM stock_list") - - # 批量插入 - data = df[['code', 'name']].to_dict('records') - cursor.executemany( - "INSERT INTO stock_list (code, name) VALUES (:code, :name)", - data - ) - self.conn.commit() - except sqlite3.Error as e: - logger.error(f"Database error saving stock list: {e}") - except Exception as e: - logger.error(f"Unexpected error saving stock list: {e}") - - def search_stock(self, query: str, limit: int = 5) -> List[Dict]: - """模糊搜索股票代码或名称""" - cursor = self.conn.cursor() - wild = f"%{query}%" - cursor.execute(""" - SELECT code, name FROM stock_list - WHERE code LIKE ? OR name LIKE ? - LIMIT ? - """, (wild, wild, limit)) - return [dict(row) for row in cursor.fetchall()] - - def get_stock_by_code(self, code: str) -> Optional[Dict[str, str]]: - """精确按代码获取股票信息。 - - Args: - code: 股票代码(A股6位 / 港股5位),必须为纯数字字符串。 - - Returns: - dict: {"code": str, "name": str} 或 None。 - """ - if not code: - return None - clean = "".join([c for c in str(code).strip() if c.isdigit()]) - if not clean: - return None - - cursor = self.conn.cursor() - cursor.execute("SELECT code, name FROM stock_list WHERE code = ? LIMIT 1", (clean,)) - row = cursor.fetchone() - return dict(row) if row else None - - def save_stock_prices(self, ticker: str, df: pd.DataFrame): - """保存股价历史数据""" - if df.empty: - return - - cursor = self.conn.cursor() - - # 确保 DataFrame 有必要的列 - required_cols = ['date', 'open', 'close', 'high', 'low', 'volume', 'change_pct'] - for col in required_cols: - if col not in df.columns: - logger.warning(f"Missing column {col} in stock data for {ticker}") - return - - try: - for _, row in df.iterrows(): - cursor.execute(""" - INSERT OR REPLACE INTO stock_prices - (ticker, date, open, close, high, low, volume, change_pct) - VALUES (?, ?, ?, ?, ?, ?, ?, ?) - """, ( - ticker, - row['date'], - row['open'], - row['close'], - row['high'], - row['low'], - row['volume'], - row['change_pct'] - )) - self.conn.commit() - except sqlite3.Error as e: - logger.error(f"Database error saving stock prices for {ticker}: {e}") - except Exception as e: - logger.error(f"Unexpected error saving stock prices for {ticker}: {e}") - - def get_stock_prices(self, ticker: str, start_date: str, end_date: str) -> pd.DataFrame: - """获取指定日期范围的股价数据""" - cursor = self.conn.cursor() - - cursor.execute(""" - SELECT * FROM stock_prices - WHERE ticker = ? AND date >= ? AND date <= ? - ORDER BY date - """, (ticker, start_date, end_date)) - - rows = cursor.fetchall() - if not rows: - return pd.DataFrame() - - columns = ['ticker', 'date', 'open', 'close', 'high', 'low', 'volume', 'change_pct'] - return pd.DataFrame([dict(row) for row in rows], columns=columns) - - def execute_query(self, query: str, params: tuple = ()) -> List[Any]: - """执行自定义 SQL 查询""" - try: - cursor = self.conn.cursor() - cursor.execute(query, params) - if query.strip().upper().startswith("SELECT"): - return cursor.fetchall() - else: - self.conn.commit() - return [] - except sqlite3.Error as e: - logger.error(f"SQL execution failed (Database error): {e}") - return [] - except Exception as e: - logger.error(f"SQL execution failed (Unexpected error): {e}") - return [] - - # --- 投资信号操作 (ISQ Framework) --- - - def save_signal(self, signal: Dict[str, Any]): - """保存投资信号""" - cursor = self.conn.cursor() - created_at = datetime.now().isoformat() - - cursor.execute(""" - INSERT OR REPLACE INTO signals - (signal_id, title, summary, transmission_chain, sentiment_score, - confidence, intensity, expected_horizon, price_in_status, - impact_tickers, industry_tags, sources, user_id, created_at) - VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?) - """, ( - signal.get('signal_id'), - signal.get('title'), - signal.get('summary'), - json.dumps(signal.get('transmission_chain', [])), - signal.get('sentiment_score', 0.0), - signal.get('confidence', 0.0), - signal.get('intensity', 1), - signal.get('expected_horizon', 'T+0'), - signal.get('price_in_status', '未知'), - json.dumps(signal.get('impact_tickers', [])), - json.dumps(signal.get('industry_tags', [])), - json.dumps(signal.get('sources', [])), - signal.get('user_id'), - created_at - )) - self.conn.commit() - - def get_recent_signals(self, limit: int = 20, user_id: Optional[str] = None) -> List[Dict]: - """获取最近的投资信号""" - cursor = self.conn.cursor() - if user_id: - cursor.execute("SELECT * FROM signals WHERE user_id = ? ORDER BY created_at DESC LIMIT ?", (user_id, limit)) - else: - cursor.execute("SELECT * FROM signals ORDER BY created_at DESC LIMIT ?", (limit,)) - rows = cursor.fetchall() - - signals = [] - for row in rows: - d = dict(row) - # 解析 JSON 字段 - for field in ['transmission_chain', 'impact_tickers', 'industry_tags', 'sources']: - if d.get(field): - try: - d[field] = json.loads(d[field]) - except: - pass - signals.append(d) - return signals - - def close(self): - if self.conn: - self.conn.close() - logger.info("Database connection closed.") - diff --git a/skills/alphaear-signal-tracker/scripts/utils/hybrid_search.py b/skills/alphaear-signal-tracker/scripts/utils/hybrid_search.py deleted file mode 100644 index c597fee..0000000 --- a/skills/alphaear-signal-tracker/scripts/utils/hybrid_search.py +++ /dev/null @@ -1,216 +0,0 @@ -import numpy as np -import os -from typing import List, Dict, Any, Optional, Union -from rank_bm25 import BM25Okapi -from loguru import logger -from sentence_transformers import SentenceTransformer -from sklearn.metrics.pairwise import cosine_similarity - -class HybridSearcher: - """ - 统一混合检索引擎 (Hybrid RAG) - 实现 BM25 (文本) + 向量 (语义) 的融合搜索 (RRF) - """ - - def __init__(self, data: List[Dict[str, Any]], text_fields: List[str] = ["title", "content"], model_name: str = None): - """ - 初始化搜索器 - - Args: - data: 数据列表,每个元素为 Dict - text_fields: 用于建立索引的文本字段 - model_name: 向量模型名称,默认使用 paraphrase-multilingual-MiniLM-L12-v2 - """ - self.data = data - self.text_fields = text_fields - self._corpus = [] - self._bm25 = None - self._vector_model = None - self._embeddings = None - self._fitted = False - self._vector_fitted = False - - # 默认模型 - self.model_name = model_name or os.getenv("EMBEDDING_MODEL", "paraphrase-multilingual-MiniLM-L12-v2") - - if data: - self._prepare_corpus() - self._fit_bm25() - # 延迟加载向量模型,仅在需要时或初始化时显式调用 - # self._fit_vector() - - def _prepare_corpus(self): - """准备语料库用于分词""" - import jieba # 使用 jieba 进行中文分词 - - self._corpus = [] - self._full_texts = [] - for item in self.data: - text = " ".join([str(item.get(field, "")) for field in self.text_fields]) - self._full_texts.append(text) - # 中文分词优化 - tokens = list(jieba.cut(text)) - self._corpus.append(tokens) - - def _fit_bm25(self): - """训练 BM25 模型""" - if self._corpus: - self._bm25 = BM25Okapi(self._corpus) - self._fitted = True - logger.info(f"✅ BM25 index fitted with {len(self.data)} documents") - - def _fit_vector(self): - """训练向量模型并生成 Embeddings""" - if not self.data: - return - - try: - logger.info(f"📡 Loading embedding model: {self.model_name}...") - self._vector_model = SentenceTransformer(self.model_name) - logger.info(f"🧠 Encoding {len(self._full_texts)} documents...") - self._embeddings = self._vector_model.encode(self._full_texts, show_progress_bar=False) - self._vector_fitted = True - logger.info("✅ Vector index fitted successfully") - except Exception as e: - logger.error(f"❌ Failed to fit vector index: {e}") - self._vector_fitted = False - - def _compute_rrf(self, rank_lists: List[List[int]], k: int = 60) -> List[tuple]: - """ - 计算 Reciprocal Rank Fusion (RRF) - - Args: - rank_lists: 多个排序后的索引列表 - k: RRF 常数,默认 60 - """ - scores = {} - for rank_list in rank_lists: - for rank, idx in enumerate(rank_list): - if idx not in scores: - scores[idx] = 0 - scores[idx] += 1.0 / (k + rank + 1) - - # 按分数排序 - sorted_indices = sorted(scores.items(), key=lambda x: x[1], reverse=True) - return sorted_indices - - def search(self, query: str, top_n: int = 5, use_vector: bool = False) -> List[Dict[str, Any]]: - """ - 执行混合搜索 - - Args: - query: 搜索关键词 - top_n: 返回结果数量 - use_vector: 是否启用向量搜索 - """ - if not self._fitted or not query: - return [] - - import jieba - query_tokens = list(jieba.cut(query)) - - # 1. BM25 搜索结果 - bm25_scores = self._bm25.get_scores(query_tokens) - bm25_rank = np.argsort(bm25_scores)[::-1].tolist() - - rank_lists = [bm25_rank] - - # 2. 向量搜索逻辑 - if use_vector: - if not self._vector_fitted: - self._fit_vector() - - if self._vector_fitted: - query_embedding = self._vector_model.encode([query], show_progress_bar=False) - similarities = cosine_similarity(query_embedding, self._embeddings)[0] - vector_rank = np.argsort(similarities)[::-1].tolist() - rank_lists.append(vector_rank) - else: - logger.warning("Vector search requested but model not fitted, falling back to BM25") - - # 3. 融合排序 (RRF) - if len(rank_lists) > 1: - rrf_results = self._compute_rrf(rank_lists) - # RRF 返回 (idx, score) 列表 - final_rank = [idx for idx, score in rrf_results] - else: - final_rank = bm25_rank - - # 返回前 top_n 条结果 - results = [self.data[idx].copy() for idx in final_rank[:top_n]] - - # 为每个结果注入相关性评分 - for i, res in enumerate(results): - try: - original_idx = final_rank[i] - res["_search_score"] = bm25_scores[original_idx] - if use_vector and self._vector_fitted: - res["_vector_score"] = float(similarities[original_idx]) - except: - res["_search_score"] = 0 - - return results - -class InMemoryRAG(HybridSearcher): - """专门用于 ReportAgent 跨章节检索的内存态 RAG""" - - def search(self, query: str, top_n: int = 3, use_vector: bool = True) -> List[Dict[str, Any]]: - """默认开启向量搜索的内存检索""" - return super().search(query, top_n=top_n, use_vector=use_vector) - - def update_data(self, new_data: List[Dict[str, Any]]): - """动态更新数据并重新训练索引""" - self.data = new_data - self._prepare_corpus() - self._fit_bm25() - # 如果之前已经加载过向量模型,则更新向量索引 - if self._vector_model: - self._fit_vector() - logger.info(f"🔄 InMemoryRAG updated with {len(new_data)} items") - -class LocalNewsSearch(HybridSearcher): - """持久态 RAG:检索数据库中的历史新闻""" - - def __init__(self, db_manager): - """ - Args: - db_manager: DatabaseManager 实例 - """ - self.db = db_manager - # 初始时不加载数据,需调用 load_history - super().__init__([], ["title", "content"]) - - def load_history(self, days: int = 30, limit: int = 1000): - """从数据库加载最近 N 天的新闻构建索引""" - try: - # 假设 db_manager 有 execute_query - query = f"SELECT title, content, publish_time, source FROM daily_news ORDER BY publish_time DESC LIMIT ?" - results = self.db.execute_query(query, (limit,)) - - data = [] - for row in results: - # 转换 Row 为 Dict - if hasattr(row, 'keys'): - item = dict(row) - else: - item = { - "title": row[0], - "content": row[1], - "publish_time": row[2], - "source": row[3] - } - data.append(item) - - self.data = data - self._prepare_corpus() - self._fit_bm25() - # 默认不立即训练向量,等到第一次搜索时按需训练 - logger.info(f"📚 LocalNewsSearch loaded {len(data)} items from history") - except Exception as e: - logger.error(f"Failed to load history for search: {e}") - - def search(self, query: str, top_n: int = 5, use_vector: bool = True) -> List[Dict[str, Any]]: - """执行本地历史搜索,默认开启向量搜索""" - if not self.data: - self.load_history() - return super().search(query, top_n=top_n, use_vector=use_vector) diff --git a/skills/alphaear-signal-tracker/scripts/utils/json_utils.py b/skills/alphaear-signal-tracker/scripts/utils/json_utils.py deleted file mode 100644 index c29aab2..0000000 --- a/skills/alphaear-signal-tracker/scripts/utils/json_utils.py +++ /dev/null @@ -1,180 +0,0 @@ -import ast -import json -import re -from typing import Optional, Any -from loguru import logger - -def _strip_comments(text: str) -> str: - """ - Safely remove C-style comments (// and /* */) from JSON-like text, - preserving strings (including URLs like http://). - """ - result = [] - i = 0 - n = len(text) - in_string = False - escape = False - - while i < n: - char = text[i] - - if in_string: - if char == '\\': - escape = not escape - elif char == '"' and not escape: - in_string = False - else: - escape = False - result.append(char) - i += 1 - continue - - # Not in string - if char == '"': - in_string = True - result.append(char) - i += 1 - continue - - # Check for // comment - if i + 1 < n and text[i:i+2] == '//': - i += 2 - while i < n and text[i] != '\n': - i += 1 - continue - - # Check for /* comment - if i + 1 < n and text[i:i+2] == '/*': - i += 2 - while i + 1 < n and text[i:i+2] != '*/': - i += 1 - i += 2 - continue - - result.append(char) - i += 1 - - return ''.join(result) - -def extract_json(text: str) -> Optional[Any]: - """ - 更加鲁棒的 JSON 提取工具。 - 处理: - 1. Markdown 代码块 (```json ... ```) - 2. 首尾多余字符 - 3. 同一个文本中多个 JSON 对象 (仅提取第一个) - 4. 简单的 JSON 修复 (末尾逗号等) - 5. C 风格注释 (// 和 /* */) - """ - if not text: - return None - - # 1. 清理明显的 Markdown 包装 - text = text.strip() - - # 先尝试精确匹配 ```json ... ``` 或 ```...``` - md_match = re.search(r'```(?:json)?\s*\n?(.*?)\n?```', text, re.DOTALL) - if md_match: - text = md_match.group(1).strip() - elif text.startswith("```"): - # 回退:如果开头有 ``` 但没完整匹配 - text = re.sub(r'^```[a-z]*\n?', '', text) - text = re.sub(r'\n?```\s*$', '', text) - - # 2. 寻找第一个 JSON 起始符 { 或 [ - start_brace = text.find('{') - start_bracket = text.find('[') - - if start_brace == -1 and start_bracket == -1: - return None - - start_idx = start_brace if (start_bracket == -1 or (start_brace != -1 and start_brace < start_bracket)) else start_bracket - - # 2.5 预处理:修复一些极其常见的 LLM 错误 - potential_json = text[start_idx:].strip() - - # remove comments safely - potential_json = _strip_comments(potential_json) - - # b. 修复缺失开头引号的键: nodes": [ -> "nodes": [ - # 匹配模式: (空白或换行) 单词 紧跟引号和冒号 - potential_json = re.sub(r'([\{\,]\s*)([a-zA-Z_]\w*)\"\s*:', r'\1"\2":', potential_json) - - # c. 修复缺失末尾引号的键: "nodes: [ -> "nodes": [ - potential_json = re.sub(r'([\{\,]\s*)\"([a-zA-Z_]\w*)\s*:', r'\1"\2":', potential_json) - - # d. 修复完全缺失引号的键: nodes: [ -> "nodes": [ - # 注意避免匹配到像 http:// 这种内容,所以限定在 { 或 , 之后 - potential_json = re.sub(r'([\{\,]\s*)([a-zA-Z_]\w*)\s*:', r'\1"\2":', potential_json) - - # 3. 使用 raw_decode 尝试解析 - decoder = json.JSONDecoder() - - # 首先尝试直接解析(不做任何预处理) - try: - obj = json.loads(potential_json) - return obj - except json.JSONDecodeError: - pass - - # 简单预处理:移除对象/列表末位多余逗号 - processed_json = re.sub(r',\s*([\]}])', r'\1', potential_json) - - try: - obj, end_pos = decoder.raw_decode(processed_json) - return obj - except json.JSONDecodeError: - pass - - # e. 修复未终止的字符串字面量问题:移除值中的实际换行符 - # LLM 可能在字符串值中生成包含真实 newline 的内容,导致 JSON 非法 - def fix_multiline_strings(s): - # 简单策略:将字符串值内的换行替换为空格 - lines = s.split('\n') - result = [] - in_string = False - for line in lines: - # 计算未转义的引号数 - quote_count = line.count('"') - line.count('\\"') - if in_string: - result[-1] += ' ' + line.strip() - else: - result.append(line) - - if quote_count % 2 == 1: - in_string = not in_string - return '\n'.join(result) - - fixed_json = fix_multiline_strings(processed_json) - - try: - obj, end_pos = decoder.raw_decode(fixed_json) - return obj - except json.JSONDecodeError: - try: - # 4. 尝试处理单引号问题 (JSON 规范要求双引号,但 LLM 常输出单引号) - # 这是一个简单的替换技巧,仅针对像 {'key': 'value'} 这样的结构 - # 注意:这可能会破坏包含单引号的字符串值,所以作为较后的回退 - fix_quotes = re.sub(r"'(.*?)':", r'"\1":', processed_json) # 修复键 - fix_quotes = re.sub(r":\s*'(.*?)'", r': "\1"', fix_quotes) # 修复简单值 - obj, end_pos = decoder.raw_decode(fix_quotes) - return obj - except (json.JSONDecodeError, TypeError): - try: - # 5. 使用 ast.literal_eval 作为终极回退 (处理 Python 字典格式) - # 提取第一个匹配的括号对内容 - # 寻找匹配的 { } - stack = [] - for i, char in enumerate(potential_json): - if char == '{': stack.append('{') - elif char == '}': - if stack: stack.pop() - if not stack: - content = potential_json[:i+1] - return ast.literal_eval(content) - except (ValueError, SyntaxError, MemoryError) as e: - logger.warning(f"All JSON extraction attempts failed: {e}") - except Exception as e: - logger.error(f"Unexpected error during JSON extraction: {e}") - - return None diff --git a/skills/alphaear-signal-tracker/scripts/utils/llm/capability.py b/skills/alphaear-signal-tracker/scripts/utils/llm/capability.py deleted file mode 100644 index d07ca4f..0000000 --- a/skills/alphaear-signal-tracker/scripts/utils/llm/capability.py +++ /dev/null @@ -1,85 +0,0 @@ -import os -from typing import Optional, List, Dict, Any -from agno.agent import Agent -from agno.models.base import Model -from loguru import logger -from ..llm.factory import get_model - - -def test_tool_call_support(model: Model) -> bool: - """ - 测试模型是否支持原生的 Tool Call (Function Calling)。 - 通过尝试执行一个简单的加法工具来验证。 - """ - - def get_current_weather(location: str): - """获取指定地点的天气""" - return f"{location} 的天气是晴天,25度。" - - test_agent = Agent( - model=model, - tools=[get_current_weather], - instructions="请调用工具查询北京的天气,并直接返回工具的输出结果。", - ) - - try: - # 运行一个简单的任务,观察是否触发了 tool_call - response = test_agent.run("北京天气怎么样?") - - # 检查 response 中是否包含 tool_calls - # Agno 的 RunResponse 对象通常包含 messages,我们可以检查最后几条消息 - has_tool_call = False - for msg in response.messages: - if hasattr(msg, "tool_calls") and msg.tool_calls: - has_tool_call = True - break - - if has_tool_call: - logger.info(f"✅ Model {model.id} supports native tool calling.") - return True - else: - # 如果没有 tool_calls 但返回了正确答案,可能是模型通过纯文本模拟了工具调用(ReAct) - # 或者根本没用工具。对于原生支持的判断,我们坚持要求有 tool_calls 结构。 - logger.warning( - f"⚠️ Model {model.id} did NOT use native tool calling structure." - ) - return False - - except Exception as e: - logger.error(f"❌ Error testing tool call for {model.id}: {e}") - return False - - -class ModelCapabilityRegistry: - """ - 模型能力注册表,用于缓存和管理不同模型的能力测试结果。 - """ - - _cache = {} - - @classmethod - def get_capabilities( - cls, provider: str, model_id: str, **kwargs - ) -> Dict[str, bool]: - key = f"{provider}:{model_id}" - if key not in cls._cache: - logger.info(f"🔍 Testing capabilities for {key}...") - model = get_model(provider, model_id, **kwargs) - supports_tool_call = test_tool_call_support(model) - cls._cache[key] = {"supports_tool_call": supports_tool_call} - return cls._cache[key] - - -if __name__ == "__main__": - import os - from skills._env_loader import load_unified_env - - load_unified_env() - - # 测试当前配置的模型 - p = os.getenv("LLM_PROVIDER", "minimax") - m = os.getenv("LLM_MODEL", "Qwen") - - print(f"Testing {p}/{m}...") - res = ModelCapabilityRegistry.get_capabilities(p, m) - print(f"Result: {res}") diff --git a/skills/alphaear-signal-tracker/scripts/utils/llm/factory.py b/skills/alphaear-signal-tracker/scripts/utils/llm/factory.py deleted file mode 100644 index 09b6ea5..0000000 --- a/skills/alphaear-signal-tracker/scripts/utils/llm/factory.py +++ /dev/null @@ -1,114 +0,0 @@ -import os -from agno.models.openai import OpenAIChat -from agno.models.ollama import Ollama -from agno.models.dashscope import DashScope -from agno.models.deepseek import DeepSeek -from agno.models.openrouter import OpenRouter - -def get_model(model_provider: str, model_id: str, **kwargs): - """ - Factory to get the appropriate LLM model. - - Args: - model_provider: "openai", "ollama", "deepseek" - model_id: The specific model ID (e.g., "gpt-4o", "llama3", "deepseek-chat") - **kwargs: Additional arguments for the model constructor - """ - if model_provider == "openai": - return OpenAIChat(id=model_id, **kwargs) - - elif model_provider == "ollama": - return Ollama(id=model_id, **kwargs) - - elif model_provider == "deepseek": - # DeepSeek is OpenAI compatible - api_key = os.getenv("DEEPSEEK_API_KEY") - if not api_key: - print("Warning: DEEPSEEK_API_KEY not set.") - - return DeepSeek( - id=model_id, - api_key=api_key, - **kwargs - ) - elif model_provider == "dashscope": - api_key = os.getenv("DASHSCOPE_API_KEY") - if not api_key: - print("Warning: DASHSCOPE_API_KEY not set.") - - return DashScope( - id=model_id, - base_url="https://dashscope.aliyuncs.com/compatible-mode/v1", - api_key=api_key, - **kwargs - ) - elif model_provider == 'openrouter': - api_key = os.getenv("OPENROUTER_API_KEY") - if not api_key: - print('Warning: OPENROUTER_API_KEY not set.') - - return OpenRouter( - id=model_id, - api_key=api_key, - **kwargs - ) - - elif model_provider == 'zai': - api_key = os.getenv("ZAI_KEY_API") - if not api_key: - print('Warning: ZAI_KEY_API not set.') - - # role_map to ensure compatibility. - default_role_map = { - "system": "system", - "user": "user", - "assistant": "assistant", - "tool": "tool", - "model": "assistant", - } - - # Allow callers to override role_map via kwargs, otherwise use default - role_map = kwargs.pop("role_map", default_role_map) - - return OpenAIChat( - id=model_id, - base_url="https://api.z.ai/api/paas/v4", - api_key=api_key, - timeout=60, - role_map=role_map, - extra_body={"enable_thinking": False}, # TODO: one more setting for thinking - **kwargs - ) - - elif model_provider == 'ust': - api_key = os.getenv("UST_KEY_API") - if not api_key: - print('Warning: UST_KEY_API not set.') - - # Some UST-compatible endpoints expect the standard OpenAI role names - # (e.g. "system", "user", "assistant") rather than Agno's default - # mapping which maps "system" -> "developer". Provide an explicit - # role_map to ensure compatibility. - default_role_map = { - "system": "system", - "user": "user", - "assistant": "assistant", - "tool": "tool", - "model": "assistant", - } - - # Allow callers to override role_map via kwargs, otherwise use default - role_map = kwargs.pop("role_map", default_role_map) - - return OpenAIChat( - id=model_id, - api_key=api_key, - base_url=os.getenv("UST_URL"), - role_map=role_map, - extra_body={"enable_thinking": False}, # TODO: one more setting for thinking - **kwargs - ) - - else: - raise ValueError(f"Unknown model provider: {model_provider}") - diff --git a/skills/alphaear-signal-tracker/scripts/utils/llm/router.py b/skills/alphaear-signal-tracker/scripts/utils/llm/router.py deleted file mode 100644 index 8c69958..0000000 --- a/skills/alphaear-signal-tracker/scripts/utils/llm/router.py +++ /dev/null @@ -1,80 +0,0 @@ -import os -from typing import Optional, List, Dict, Any, Union -from agno.models.base import Model -from loguru import logger -from ..llm.factory import get_model -from ..llm.capability import ModelCapabilityRegistry -from skills._env_loader import load_unified_env - -load_unified_env() - - -class ModelRouter: - """ - 模型路由管理器 - - 功能: - 1. 管理“推理/写作模型” (Reasoning Model) 和“工具调用模型” (Tool Model)。 - 2. 根据任务需求自动选择合适的模型。 - """ - - def __init__(self): - # 默认从环境变量读取 - self.reasoning_provider = os.getenv( - "REASONING_MODEL_PROVIDER", os.getenv("LLM_PROVIDER", "openai") - ) - self.reasoning_id = os.getenv( - "REASONING_MODEL_ID", os.getenv("LLM_MODEL", "gpt-4o") - ) - self.reasoning_host = os.getenv("REASONING_MODEL_HOST", os.getenv("LLM_HOST")) - - self.tool_provider = os.getenv("TOOL_MODEL_PROVIDER", self.reasoning_provider) - self.tool_id = os.getenv("TOOL_MODEL_ID", self.reasoning_id) - self.tool_host = os.getenv("TOOL_MODEL_HOST", self.reasoning_host) - - self._reasoning_model = None - self._tool_model = None - - logger.info( - f"🤖 ModelRouter initialized: Reasoning={self.reasoning_id} ({self.reasoning_host or 'default'}), Tool={self.tool_id} ({self.tool_host or 'default'})" - ) - - def get_reasoning_model(self, **kwargs) -> Model: - if not self._reasoning_model: - # 优先使用路由配置的 host - if self.reasoning_host and "host" not in kwargs: - kwargs["host"] = self.reasoning_host - self._reasoning_model = get_model( - self.reasoning_provider, self.reasoning_id, **kwargs - ) - return self._reasoning_model - - def get_tool_model(self, **kwargs) -> Model: - if not self._tool_model: - # 优先使用路由配置的 host - if self.tool_host and "host" not in kwargs: - kwargs["host"] = self.tool_host - - # 检查 tool_model 是否真的支持 tool call - caps = ModelCapabilityRegistry.get_capabilities( - self.tool_provider, self.tool_id, **kwargs - ) - if not caps["supports_tool_call"]: - logger.warning( - f"⚠️ Configured tool model {self.tool_id} might not support native tool calls! Consider using ReAct mode or a different model." - ) - - self._tool_model = get_model(self.tool_provider, self.tool_id, **kwargs) - return self._tool_model - - def get_model_for_agent(self, has_tools: bool = False, **kwargs) -> Model: - """ - 根据 Agent 是否包含工具来返回合适的模型。 - """ - if has_tools: - return self.get_tool_model(**kwargs) - return self.get_reasoning_model(**kwargs) - - -# 全局单例 -router = ModelRouter() diff --git a/skills/alphaear-signal-tracker/scripts/utils/logging_setup.py b/skills/alphaear-signal-tracker/scripts/utils/logging_setup.py deleted file mode 100644 index 9a2ca62..0000000 --- a/skills/alphaear-signal-tracker/scripts/utils/logging_setup.py +++ /dev/null @@ -1,45 +0,0 @@ -import os -import sys -from datetime import datetime -from typing import Optional - -from loguru import logger - - -def setup_file_logging( - run_id: str, - log_dir: str = "logs", - level: str = "INFO", - retention: str = "10 days", - rotation: str = "20 MB", -) -> str: - """Configure Loguru to log to stderr + a per-run file. - - Returns the log file path. - """ - os.makedirs(log_dir, exist_ok=True) - - # Remove default handler to avoid duplicate logs. - logger.remove() - - # Console - logger.add(sys.stderr, level=level, backtrace=False, diagnose=False) - - # File (safe for multi-thread via enqueue) - log_path = os.path.join(log_dir, f"signalflux_{run_id}.log") - logger.add( - log_path, - level=level, - rotation=rotation, - retention=retention, - enqueue=True, - backtrace=True, - diagnose=False, - encoding="utf-8", - ) - return log_path - - -def make_run_id(prefix: Optional[str] = None) -> str: - ts = datetime.now().strftime("%Y%m%d_%H%M%S") - return f"{prefix}_{ts}" if prefix else ts diff --git a/skills/alphaear-signal-tracker/scripts/utils/md_to_html.py b/skills/alphaear-signal-tracker/scripts/utils/md_to_html.py deleted file mode 100644 index 314c282..0000000 --- a/skills/alphaear-signal-tracker/scripts/utils/md_to_html.py +++ /dev/null @@ -1,185 +0,0 @@ -import markdown -import os -from loguru import logger - -def convert_md_to_html(md_content: str, title: str = "AlphaEar Report") -> str: - """ - 将 Markdown 转换为带样式的 HTML - """ - # 转换 Markdown 为 HTML - # 启用 table, toc 等扩展 - # 使用 'md_in_html' 来正确处理 markdown 中的 HTML 块 - html_body = markdown.markdown( - md_content, - extensions=['extra', 'toc', 'nl2br', 'md_in_html'] - ) - - - # 简单的 Premium CSS 模板 - html_template = f""" - - - - - - {title} - - - -
- {html_body} - -
- - - """ - return html_template - -def save_report_as_html(md_path: str, output_path: str = None): - if not output_path: - output_path = md_path.replace(".md", ".html") - - try: - with open(md_path, "r", encoding="utf-8") as f: - md_content = f.read() - - title = "AlphaEar 市场研报" - # 尝试从第一行获取标题 - lines = md_content.split('\n') - if lines and lines[0].startswith('# '): - title = lines[0].replace('# ', '').strip() - - html_content = convert_md_to_html(md_content, title) - - with open(output_path, "w", encoding="utf-8") as f: - f.write(html_content) - - logger.info(f"✅ HTML Report saved to: {output_path}") - return output_path - except Exception as e: - logger.error(f"Failed to convert report to HTML: {e}") - return None - -if __name__ == "__main__": - import sys - if len(sys.argv) > 1: - save_report_as_html(sys.argv[1]) - else: - print("Usage: python3 md_to_html.py ") diff --git a/skills/alphaear-signal-tracker/scripts/utils/news_tools.py b/skills/alphaear-signal-tracker/scripts/utils/news_tools.py deleted file mode 100644 index e833e2e..0000000 --- a/skills/alphaear-signal-tracker/scripts/utils/news_tools.py +++ /dev/null @@ -1,256 +0,0 @@ -import requests -from requests.exceptions import RequestException, Timeout -import json -import time -from datetime import datetime -from typing import List, Dict, Optional -from loguru import logger -from .database_manager import DatabaseManager -from .content_extractor import ContentExtractor - -class NewsNowTools: - """热点新闻获取工具 - 接入 NewsNow API 与 Jina 内容提取""" - - BASE_URL = "https://newsnow.busiyi.world" - SOURCES = { - # 金融类 - "cls": "财联社", - "wallstreetcn": "华尔街见闻", - "xueqiu": "雪球热榜", - # 综合/社交 - "weibo": "微博热搜", - "zhihu": "知乎热榜", - "baidu": "百度热搜", - "toutiao": "今日头条", - "douyin": "抖音热榜", - "thepaper": "澎湃新闻", - # 科技类 - "36kr": "36氪", - "ithome": "IT之家", - "v2ex": "V2EX", - "juejin": "掘金", - "hackernews": "Hacker News", - } - - - def __init__(self, db: DatabaseManager): - self.db = db - self.user_agent = ( - "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) " - "AppleWebKit/537.36 (KHTML, like Gecko) Chrome/131.0.0.0 Safari/537.36" - ) - self.extractor = ContentExtractor() - # Simple in-memory cache: source_id -> {"time": timestamp, "data": []} - self._cache = {} - - def fetch_hot_news(self, source_id: str, count: int = 15, fetch_content: bool = False) -> List[Dict]: - """ - 从指定新闻源获取热点新闻列表(支持5分钟缓存)。 - """ - # 1. Check cache validity (5 minutes) - cache_key = f"{source_id}_{count}" - cached = self._cache.get(cache_key) - now = time.time() - - if cached and (now - cached["time"] < 300): - logger.info(f"⚡ Using cached news for {source_id} (Age: {int(now - cached['time'])}s)") - return cached["data"] - - try: - url = f"{self.BASE_URL}/api/s?id={source_id}" - response = requests.get(url, headers={"User-Agent": self.user_agent}, timeout=30) - if response.status_code == 200: - data = response.json() - items = data.get("items", [])[:count] - processed_items = [] - for i, item in enumerate(items, 1): - item_url = item.get("url", "") - content = "" - if fetch_content and item_url: - content = self.extractor.extract_with_jina(item_url) or "" - - processed_items.append({ - "id": item.get("id") or f"{source_id}_{int(time.time())}_{i}", - "source": source_id, - "rank": i, - "title": item.get("title", ""), - "url": item_url, - "content": content, - "publish_time": item.get("publish_time"), - "meta_data": item.get("extra", {}) - }) - - # Update Cache - self._cache[cache_key] = {"time": now, "data": processed_items} - logger.info(f"✅ Fetched and cached news for {source_id}") - - self.db.save_daily_news(processed_items) - return processed_items - else: - logger.error(f"NewsNow API Error: {response.status_code}") - # Fallback to stale cache if available - if cached: - logger.warning(f"⚠️ API failed, using stale cache for {source_id}") - return cached["data"] - return [] - except Timeout: - logger.error(f"Timeout fetching hot news from {source_id}") - if cached: - logger.warning(f"⚠️ Timeout, using stale cache for {source_id}") - return cached["data"] - return [] - except RequestException as e: - logger.error(f"Network error fetching hot news from {source_id}: {e}") - if cached: - logger.warning(f"⚠️ Network check failed, using stale cache for {source_id}") - return cached["data"] - return [] - except json.JSONDecodeError: - logger.error(f"Failed to parse JSON response from NewsNow for {source_id}") - return [] - except Exception as e: - logger.error(f"Unexpected error fetching hot news from {source_id}: {e}") - return [] - - def fetch_news_content(self, url: str) -> Optional[str]: - """ - 使用 Jina Reader 抓取指定 URL 的网页正文内容。 - - Args: - url: 需要抓取内容的完整网页 URL,必须以 http:// 或 https:// 开头。 - - Returns: - 提取的网页正文内容 (Markdown 格式),如果失败则返回 None。 - """ - return self.extractor.extract_with_jina(url) - - def get_unified_trends(self, sources: Optional[List[str]] = None) -> str: - """ - 获取多平台综合热点报告,自动聚合多个新闻源的热门内容。 - - Args: - sources: 要扫描的新闻源列表。可选值按类别: - **金融类**: "cls", "wallstreetcn", "xueqiu" - **综合类**: "weibo", "zhihu", "baidu", "toutiao", "douyin", "thepaper" - **科技类**: "36kr", "ithome", "v2ex", "juejin", "hackernews" - - Returns: - 格式化的 Markdown 热点汇总报告,包含各平台 Top 10 热点标题和链接。 - """ - sources = sources or ["weibo", "zhihu", "wallstreetcn"] - all_news = [] - for src in sources: - all_news.extend(self.fetch_hot_news(src)) - time.sleep(0.2) - - if not all_news: - return "❌ 未能获取到热点数据" - - report = f"# 实时全网热点汇总 ({datetime.now().strftime('%Y-%m-%d %H:%M')})\n\n" - for src in sources: - - src_name = self.SOURCES.get(src, src) - report += f"### 🔥 {src_name}\n" - src_news = [n for n in all_news if n['source'] == src] - for n in src_news[:10]: - report += f"- {n['title']} ([链接]({n['url']}))\n" - report += "\n" - - return report - - -class PolymarketTools: - """Polymarket 预测市场数据工具 - 获取热门预测市场反映公众情绪和预期""" - - BASE_URL = "https://gamma-api.polymarket.com" - - def __init__(self, db: DatabaseManager): - self.db = db - self.user_agent = "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36" - - def get_active_markets(self, limit: int = 20) -> List[Dict]: - """ - 获取活跃的预测市场,用于分析公众情绪和预期。 - - 预测市场数据可以反映: - - 公众对重大事件的预期概率 - - 市场情绪和风险偏好 - - 热门话题的关注度 - - Args: - limit: 获取的市场数量,默认 20 个。 - - Returns: - 包含预测市场信息的列表,每个市场包含: - - question: 预测问题 - - outcomes: 可能的结果 - - outcomePrices: 各结果的概率价格 - - volume: 交易量 - """ - try: - response = requests.get( - f"{self.BASE_URL}/markets", - params={"active": "true", "closed": "false", "limit": limit}, - headers={"User-Agent": self.user_agent, "Accept": "application/json"}, - timeout=30 - ) - - if response.status_code == 200: - markets = response.json() - result = [] - for m in markets: - result.append({ - "id": m.get("id"), - "question": m.get("question"), - "slug": m.get("slug"), - "outcomes": m.get("outcomes"), - "outcomePrices": m.get("outcomePrices"), - "volume": m.get("volume"), - "liquidity": m.get("liquidity"), - }) - logger.info(f"✅ 获取 {len(result)} 个预测市场") - return result - else: - logger.warning(f"⚠️ Polymarket API 返回 {response.status_code}") - return [] - except Timeout: - logger.error("Timeout fetching Polymarket markets") - return [] - except RequestException as e: - logger.error(f"Network error fetching Polymarket markets: {e}") - return [] - except json.JSONDecodeError: - logger.error("Failed to parse JSON response from Polymarket") - return [] - except Exception as e: - logger.error(f"Unexpected error fetching Polymarket markets: {e}") - return [] - - def get_market_summary(self, limit: int = 10) -> str: - """ - 获取预测市场摘要报告,用于了解当前热门话题和公众预期。 - - Args: - limit: 获取的市场数量 - - Returns: - 格式化的预测市场报告 - """ - markets = self.get_active_markets(limit) - if not markets: - return "❌ 无法获取 Polymarket 数据" - - report = f"# 🔮 Polymarket 热门预测 ({datetime.now().strftime('%Y-%m-%d %H:%M')})\n\n" - for i, m in enumerate(markets, 1): - question = m.get("question", "Unknown") - prices = m.get("outcomePrices", []) - volume = m.get("volume", 0) - - report += f"**{i}. {question}**\n" - if prices: - report += f" 概率: {prices}\n" - if volume: - report += f" 交易量: ${float(volume):,.0f}\n" - report += "\n" - - return report diff --git a/skills/alphaear-signal-tracker/scripts/utils/predictor/evaluation.py b/skills/alphaear-signal-tracker/scripts/utils/predictor/evaluation.py deleted file mode 100644 index 26c5df7..0000000 --- a/skills/alphaear-signal-tracker/scripts/utils/predictor/evaluation.py +++ /dev/null @@ -1,137 +0,0 @@ -import os -import sys -import torch -import pandas as pd -import numpy as np -import glob -from loguru import logger -from datetime import datetime, timedelta - -# Setup paths -KRONOS_DIR = os.path.dirname(os.path.abspath(__file__)) -SRC_DIR = os.path.dirname(os.path.dirname(KRONOS_DIR)) -if SRC_DIR not in sys.path: - sys.path.insert(0, SRC_DIR) - -from ..kronos.auto_synthesis_training import AutoSynthesisTrainer -from ..kronos.model import KronosPredictor -from ..visualizer import VisualizerTools -from ..schema.models import ForecastResult, KLinePoint - -class NewsModelEvaluator: - def __init__(self, model_path=None): - self.trainer = AutoSynthesisTrainer() - self.device = self.trainer.device - - if model_path is None: - # Try to find the latest model in exports/models - model_files = glob.glob(os.path.join(SRC_DIR, "exports/models/*.pt")) - if not model_files: - logger.warning("⚠️ No trained models found in exports/models/. Using base model (zero-init proj).") - else: - model_path = max(model_files, key=os.path.getctime) - - if model_path: - self.load_weights(model_path) - - def load_weights(self, path): - logger.info(f"🔄 Loading model weights from {path}...") - checkpoint = torch.load(path, map_location=self.device) - self.trainer.model.news_proj.load_state_dict(checkpoint['news_proj_state_dict']) - logger.success("✅ News projection layer loaded.") - - def evaluate_range(self, start_idx=100, end_idx=200, pred_len=5): - # 1. Fetch Tickers - res = self.trainer.db.execute_query("SELECT code FROM stock_list") - all_tickers = [row['code'] for row in res] - test_tickers = all_tickers[start_idx:end_idx] - - if not test_tickers: - logger.error(f"No tickers found in range {start_idx}-{end_idx}") - return - - logger.info(f"🚀 Evaluating News Model on stocks {start_idx} to {end_idx}...") - - # 2. Discover Shocks - shocks = self.trainer.discover_shocks(test_tickers, pred_len=pred_len) - - # 3. Associate News & Predict - self.trainer.model.eval() - predictor = KronosPredictor(self.trainer.model, self.trainer.tokenizer, device=self.device) - - save_dir = os.path.join(SRC_DIR, "exports/evaluation_results") - os.makedirs(save_dir, exist_ok=True) - - count = 0 - for shock in shocks: - summary = self.trainer.find_reason_and_verify(shock) - if not summary: - continue - - logger.info(f"📈 Testing shock: {shock['ticker']} on {shock['date']}") - - # Embedding news - news_emb = self.trainer.embedder.encode(summary) - - # Prediction - h = shock['history'] - t = shock['target'] - actuals = t['close'].values[:pred_len] - - x_ts = pd.to_datetime(h['date']) - future_dates = pd.date_range(start=x_ts.iloc[-1] + timedelta(days=1), periods=pred_len, freq='B') - y_ts = pd.Series(future_dates) - - # A. Base Prediction (No news) - p_base = predictor.predict(h, x_ts, y_ts, pred_len=pred_len, news_emb=None, verbose=False) - - # B. News-Aware Prediction - p_news = predictor.predict(h, x_ts, y_ts, pred_len=pred_len, news_emb=news_emb, verbose=False) - - # Calculate Improvement - b_preds = p_base['close'].values[:len(actuals)] - n_preds = p_news['close'].values[:len(actuals)] - b_mae = np.mean(np.abs(b_preds - actuals)) - n_mae = np.mean(np.abs(n_preds - actuals)) - improvement = (b_mae - n_mae) / (b_mae + 1e-6) * 100 - - # C. Visualize - try: - def to_kp_list(preds_df): - points = [] - for idx, row in preds_df.iterrows(): - points.append(KLinePoint( - date=str(idx)[:10], open=row['open'], high=row['high'], - low=row['low'], close=row['close'], volume=row.get('volume', 0) - )) - return points - - forecast_obj = ForecastResult( - ticker=shock['ticker'], - base_forecast=to_kp_list(p_base), - adjusted_forecast=to_kp_list(p_news), - rationale=summary - ) - - chart = VisualizerTools.generate_stock_chart( - df=h, ticker=shock['ticker'], - title=f"Test Eval: {shock['ticker']} ({shock['date']}) Imp: {improvement:.1f}%", - forecast=forecast_obj, - ground_truth=t[['date', 'open', 'high', 'low', 'close', 'volume']] - ) - - safe_date = shock['date'].replace("-", "") - filename = f"test_{shock['ticker']}_{safe_date}.html" - VisualizerTools.render_chart_to_file(chart, os.path.join(save_dir, filename)) - - logger.success(f"📊 Result for {shock['ticker']} saved. Base MAE: {b_mae:.4f}, News MAE: {n_mae:.4f}") - count += 1 - except Exception as e: - logger.error(f"Visualization failed: {e}") - - logger.info(f"🏁 Finished evaluation. {count} cases visualized in {save_dir}") - -if __name__ == "__main__": - # If you have a specific model, pass the path here. Otherwise it picks the latest. - evaluator = NewsModelEvaluator() - evaluator.evaluate_range(start_idx=100, end_idx=200, pred_len=1) diff --git a/skills/alphaear-signal-tracker/scripts/utils/predictor/kline_generate.py b/skills/alphaear-signal-tracker/scripts/utils/predictor/kline_generate.py deleted file mode 100644 index 3224c21..0000000 --- a/skills/alphaear-signal-tracker/scripts/utils/predictor/kline_generate.py +++ /dev/null @@ -1,196 +0,0 @@ -# Ref: https://github.com/shiyu-coder/Kronos - -from model import Kronos, KronosTokenizer, KronosPredictor -import pandas as pd -import sqlite3 -import torch -import matplotlib.pyplot as plt -import matplotlib.gridspec as gridspec -from pandas.tseries.offsets import BusinessDay -import numpy as np - -def get_device(): - device = "cuda" if torch.cuda.is_available() else "mps" if torch.backends.mps.is_available() else "cpu" - print(f"Using device: {device}") - return device - -def load_predictor(): - tokenizer = KronosTokenizer.from_pretrained("NeoQuasar/Kronos-Tokenizer-base") - model = Kronos.from_pretrained("NeoQuasar/Kronos-base") - device = get_device() - tokenizer = tokenizer.to(device) - model = model.to(device) - return KronosPredictor(model, tokenizer, device=device, max_context=512) - -def load_data(ticker="002111", db_path="AlphaEar/data/signal_flux.db"): - with sqlite3.connect(db_path) as conn: - df = pd.read_sql_query(f"SELECT * FROM stock_prices WHERE ticker = '{ticker}'", conn) - df['date'] = pd.to_datetime(df['date']) - df = df.sort_values('date').reset_index(drop=True) - return df - -def plot_kline_matplotlib(ax, ax_vol, dates, df, label_suffix="", color_up='#ef4444', color_down='#22c55e', alpha=1.0, is_prediction=False): - """ - 绘制 K 线图和成交量 - """ - # X axis mapping to integers for consistent spacing - x = np.arange(len(dates)) - - # K-line data - opens = df['open'].values - closes = df['close'].values - highs = df['high'].values - lows = df['low'].values - volumes = df['volume'].values - - # Width of the candlestick - width = 0.6 - - for i in range(len(x)): - color = color_up if closes[i] >= opens[i] else color_down - linestyle = '--' if is_prediction else '-' - - # Wick - ax.vlines(x[i], lows[i], highs[i], color=color, linewidth=1, alpha=alpha, linestyle=linestyle) - - # Body - rect_bottom = min(opens[i], closes[i]) - rect_height = abs(opens[i] - closes[i]) - if rect_height == 0: rect_height = 0.001 # Visual hair - - ax.add_patch(plt.Rectangle((x[i] - width/2, rect_bottom), width, rect_height, - edgecolor=color, facecolor=color if not is_prediction else 'none', - alpha=alpha, linewidth=1, linestyle=linestyle)) - - # Volume - ax_vol.bar(x[i], volumes[i], color=color, alpha=alpha * 0.5, width=width) - -def render_comparison_chart(history_df, actual_df, pred_df, title): - """ - 渲染组合图:历史 K 线 + 真值 K 线 + 预测 K 线 - """ - # Combine all dates for X axis - all_dates = pd.concat([history_df['date'], actual_df['date'] if actual_df is not None else pred_df.index.to_series()]).unique() - all_dates = sorted(all_dates) - date_to_idx = {date: i for i, date in enumerate(all_dates)} - - fig = plt.figure(figsize=(14, 8), facecolor='white') - gs = gridspec.GridSpec(2, 1, height_ratios=[3, 1], hspace=0.1) - ax_main = fig.add_subplot(gs[0]) - ax_vol = fig.add_subplot(gs[1], sharex=ax_main) - - # 1. Plot History - hist_indices = [date_to_idx[d] for d in history_df['date']] - # We use a custom x for plotting to ensure continuity - plot_kline_matplotlib(ax_main, ax_vol, history_df['date'], history_df, alpha=0.8) - - offset = len(history_df) - - # 2. Plot Actual if exists - if actual_df is not None: - # Shift indices - actual_x = np.arange(len(actual_df)) + offset - # Plotting manually to handle offset - for i in range(len(actual_df)): - idx = actual_x[i] - row = actual_df.iloc[i] - color = '#ef4444' if row['close'] >= row['open'] else '#22c55e' - ax_main.vlines(idx, row['low'], row['high'], color=color, linewidth=1, alpha=0.9) - ax_main.add_patch(plt.Rectangle((idx - 0.3, min(row['open'], row['close'])), 0.6, abs(row['open']-row['close']), - edgecolor=color, facecolor=color, alpha=0.9)) - ax_vol.bar(idx, row['volume'], color=color, alpha=0.4) - - # 3. Plot Prediction - pred_x = np.arange(len(pred_df)) + offset - for i in range(len(pred_df)): - idx = pred_x[i] - row = pred_df.iloc[i] - color = '#ff8c00' # Orange for prediction to distinguish - ax_main.vlines(idx, row['low'], row['high'], color=color, linewidth=1.5, linestyle='--') - ax_main.add_patch(plt.Rectangle((idx - 0.3, min(row['open'], row['close'])), 0.6, abs(row['open']-row['close']), - edgecolor=color, facecolor='none', linewidth=1.5, linestyle='--')) - # Plot secondary prediction line for close - if i == 0: - # Connect to history - ax_main.plot([offset-1, idx], [history_df['close'].iloc[-1], row['close']], color=color, linestyle='--', alpha=0.6) - elif i > 0: - ax_main.plot([idx-1, idx], [pred_df['close'].iloc[i-1], row['close']], color=color, linestyle='--', alpha=0.6) - - # Styling - ax_main.set_title(title, fontsize=14, fontweight='bold') - ax_main.grid(True, linestyle=':', alpha=0.6) - ax_vol.grid(True, linestyle=':', alpha=0.6) - ax_vol.set_ylabel('Volume') - ax_main.set_ylabel('Price') - - # Set X ticks - step = max(1, len(all_dates) // 10) - ax_vol.set_xticks(np.arange(0, len(all_dates), step)) - ax_vol.set_xticklabels([all_dates[i].strftime('%Y-%m-%d') for i in range(0, len(all_dates), step)], rotation=45) - - plt.tight_layout() - plt.show() - plt.close() - -def run_backtest(df, predictor, lookback, pred_len, start_index=0): - total_len = len(df) - history_start = start_index - history_end = start_index + lookback - pred_start = history_end - - available_pred_len = total_len - pred_start - if available_pred_len <= 0: return - actual_pred_len = min(pred_len, available_pred_len) - pred_end = pred_start + actual_pred_len - - x_df = df.iloc[history_start : history_end].copy() - y_true_df = df.iloc[pred_start : pred_end].copy() - y_timestamp = y_true_df['date'] - - print(f"Backtesting: {x_df['date'].iloc[0].date()} to {y_timestamp.iloc[-1].date()}") - - pred_df = predictor.predict( - df=x_df[['open', 'high', 'low', 'close', 'volume']], - x_timestamp=x_df['date'], - y_timestamp=y_timestamp, - pred_len=actual_pred_len, - T=1.0, top_p=0.9, sample_count=1 - ) - - render_comparison_chart(x_df, y_true_df, pred_df, f"Backtest: {TICKER} K-Line Comparison") - -def run_forecast(df, predictor, lookback, pred_len): - if len(df) < lookback: return - x_df = df.iloc[-lookback:].copy() - last_date = x_df['date'].iloc[-1] - future_dates = pd.date_range(start=last_date + BusinessDay(1), periods=pred_len, freq='B') - future_dates = pd.Series(future_dates) - - print(f"Forecasting: Starting from {future_dates.iloc[0].date()}") - - pred_df = predictor.predict( - df=x_df[['open', 'high', 'low', 'close', 'volume']], - x_timestamp=x_df['date'], - y_timestamp=future_dates, - pred_len=pred_len, - T=1.0, top_p=0.9, sample_count=1 - ) - - render_comparison_chart(x_df, None, pred_df, f"Forecast: {TICKER} Future K-Line") - -if __name__ == "__main__": - LOOKBACK = 20 - PRED_LEN = 10 - TICKER = '002111' - - pred_model = load_predictor() - stock_data = load_data(TICKER) - - total_rows = len(stock_data) - backtest_start = max(0, total_rows - LOOKBACK - PRED_LEN - 10) # Leave some space to see trend - - print("\n--- Running Backtest ---") - run_backtest(stock_data, pred_model, LOOKBACK, PRED_LEN, start_index=backtest_start) - - print("\n--- Running Forecast ---") - run_forecast(stock_data, pred_model, LOOKBACK, PRED_LEN) \ No newline at end of file diff --git a/skills/alphaear-signal-tracker/scripts/utils/predictor/model/__init__.py b/skills/alphaear-signal-tracker/scripts/utils/predictor/model/__init__.py deleted file mode 100644 index d10e200..0000000 --- a/skills/alphaear-signal-tracker/scripts/utils/predictor/model/__init__.py +++ /dev/null @@ -1,16 +0,0 @@ -from .kronos import KronosTokenizer, Kronos, KronosPredictor - -model_dict = { - 'kronos_tokenizer': KronosTokenizer, - 'kronos': Kronos, - 'kronos_predictor': KronosPredictor -} - - -def get_model_class(model_name): - if model_name in model_dict: - return model_dict[model_name] - else: - print(f"Model {model_name} not found in model_dict") - raise NotImplementedError - diff --git a/skills/alphaear-signal-tracker/scripts/utils/predictor/model/kronos.py b/skills/alphaear-signal-tracker/scripts/utils/predictor/model/kronos.py deleted file mode 100644 index cf8bece..0000000 --- a/skills/alphaear-signal-tracker/scripts/utils/predictor/model/kronos.py +++ /dev/null @@ -1,676 +0,0 @@ -import numpy as np -import pandas as pd -import torch -from huggingface_hub import PyTorchModelHubMixin -import sys - -from tqdm import trange - -sys.path.append("../") -from model.module import * - - -class KronosTokenizer(nn.Module, PyTorchModelHubMixin): - """ - KronosTokenizer module for tokenizing input data using a hybrid quantization approach. - - This tokenizer utilizes a combination of encoder and decoder Transformer blocks - along with the Binary Spherical Quantization (BSQuantizer) to compress and decompress input data. - - Args: - d_in (int): Input dimension. - d_model (int): Model dimension. - n_heads (int): Number of attention heads. - ff_dim (int): Feed-forward dimension. - n_enc_layers (int): Number of encoder layers. - n_dec_layers (int): Number of decoder layers. - ffn_dropout_p (float): Dropout probability for feed-forward networks. - attn_dropout_p (float): Dropout probability for attention mechanisms. - resid_dropout_p (float): Dropout probability for residual connections. - s1_bits (int): Number of bits for the pre token in BSQuantizer. - s2_bits (int): Number of bits for the post token in BSQuantizer. - beta (float): Beta parameter for BSQuantizer. - gamma0 (float): Gamma0 parameter for BSQuantizer. - gamma (float): Gamma parameter for BSQuantizer. - zeta (float): Zeta parameter for BSQuantizer. - group_size (int): Group size parameter for BSQuantizer. - - """ - - def __init__(self, d_in, d_model, n_heads, ff_dim, n_enc_layers, n_dec_layers, ffn_dropout_p, attn_dropout_p, resid_dropout_p, s1_bits, s2_bits, beta, gamma0, gamma, zeta, group_size): - - super().__init__() - self.d_in = d_in - self.d_model = d_model - self.n_heads = n_heads - self.ff_dim = ff_dim - self.enc_layers = n_enc_layers - self.dec_layers = n_dec_layers - self.ffn_dropout_p = ffn_dropout_p - self.attn_dropout_p = attn_dropout_p - self.resid_dropout_p = resid_dropout_p - - self.s1_bits = s1_bits - self.s2_bits = s2_bits - self.codebook_dim = s1_bits + s2_bits # Total dimension of the codebook after quantization - self.embed = nn.Linear(self.d_in, self.d_model) - self.head = nn.Linear(self.d_model, self.d_in) - - # Encoder Transformer Blocks - self.encoder = nn.ModuleList([ - TransformerBlock(self.d_model, self.n_heads, self.ff_dim, self.ffn_dropout_p, self.attn_dropout_p, self.resid_dropout_p) - for _ in range(self.enc_layers - 1) - ]) - # Decoder Transformer Blocks - self.decoder = nn.ModuleList([ - TransformerBlock(self.d_model, self.n_heads, self.ff_dim, self.ffn_dropout_p, self.attn_dropout_p, self.resid_dropout_p) - for _ in range(self.dec_layers - 1) - ]) - self.quant_embed = nn.Linear(in_features=self.d_model, out_features=self.codebook_dim) # Linear layer before quantization - self.post_quant_embed_pre = nn.Linear(in_features=self.s1_bits, out_features=self.d_model) # Linear layer after quantization (pre part - s1 bits) - self.post_quant_embed = nn.Linear(in_features=self.codebook_dim, out_features=self.d_model) # Linear layer after quantization (full codebook) - self.tokenizer = BSQuantizer(self.s1_bits, self.s2_bits, beta, gamma0, gamma, zeta, group_size) # BSQuantizer module - - def forward(self, x): - """ - Forward pass of the KronosTokenizer. - - Args: - x (torch.Tensor): Input tensor of shape (batch_size, seq_len, d_in). - - Returns: - tuple: A tuple containing: - - tuple: (z_pre, z) - Reconstructed outputs from decoder with s1_bits and full codebook respectively, - both of shape (batch_size, seq_len, d_in). - - torch.Tensor: bsq_loss - Loss from the BSQuantizer. - - torch.Tensor: quantized - Quantized representation from BSQuantizer. - - torch.Tensor: z_indices - Indices from the BSQuantizer. - """ - z = self.embed(x) - - for layer in self.encoder: - z = layer(z) - - z = self.quant_embed(z) # (B, T, codebook) - - bsq_loss, quantized, z_indices = self.tokenizer(z) - - quantized_pre = quantized[:, :, :self.s1_bits] # Extract the first part of quantized representation (s1_bits) - z_pre = self.post_quant_embed_pre(quantized_pre) - - z = self.post_quant_embed(quantized) - - # Decoder layers (for pre part - s1 bits) - for layer in self.decoder: - z_pre = layer(z_pre) - z_pre = self.head(z_pre) - - # Decoder layers (for full codebook) - for layer in self.decoder: - z = layer(z) - z = self.head(z) - - return (z_pre, z), bsq_loss, quantized, z_indices - - def indices_to_bits(self, x, half=False): - """ - Converts indices to bit representations and scales them. - - Args: - x (torch.Tensor): Indices tensor. - half (bool, optional): Whether to process only half of the codebook dimension. Defaults to False. - - Returns: - torch.Tensor: Bit representation tensor. - """ - if half: - x1 = x[0] # Assuming x is a tuple of indices if half is True - x2 = x[1] - mask = 2 ** torch.arange(self.codebook_dim//2, device=x1.device, dtype=torch.long) # Create a mask for bit extraction - x1 = (x1.unsqueeze(-1) & mask) != 0 # Extract bits for the first half - x2 = (x2.unsqueeze(-1) & mask) != 0 # Extract bits for the second half - x = torch.cat([x1, x2], dim=-1) # Concatenate the bit representations - else: - mask = 2 ** torch.arange(self.codebook_dim, device=x.device, dtype=torch.long) # Create a mask for bit extraction - x = (x.unsqueeze(-1) & mask) != 0 # Extract bits - - x = x.float() * 2 - 1 # Convert boolean to bipolar (-1, 1) - q_scale = 1. / (self.codebook_dim ** 0.5) # Scaling factor - x = x * q_scale - return x - - def encode(self, x, half=False): - """ - Encodes the input data into quantized indices. - - Args: - x (torch.Tensor): Input tensor of shape (batch_size, seq_len, d_in). - half (bool, optional): Whether to use half quantization in BSQuantizer. Defaults to False. - - Returns: - torch.Tensor: Quantized indices from BSQuantizer. - """ - z = self.embed(x) - for layer in self.encoder: - z = layer(z) - z = self.quant_embed(z) - - bsq_loss, quantized, z_indices = self.tokenizer(z, half=half, collect_metrics=False) - return z_indices - - def decode(self, x, half=False): - """ - Decodes quantized indices back to the input data space. - - Args: - x (torch.Tensor): Quantized indices tensor. - half (bool, optional): Whether the indices were generated with half quantization. Defaults to False. - - Returns: - torch.Tensor: Reconstructed output tensor of shape (batch_size, seq_len, d_in). - """ - quantized = self.indices_to_bits(x, half) - z = self.post_quant_embed(quantized) - for layer in self.decoder: - z = layer(z) - z = self.head(z) - return z - - -class Kronos(nn.Module, PyTorchModelHubMixin): - """ - Kronos Model. - - Args: - s1_bits (int): Number of bits for pre tokens. - s2_bits (int): Number of bits for post tokens. - n_layers (int): Number of Transformer blocks. - d_model (int): Dimension of the model's embeddings and hidden states. - n_heads (int): Number of attention heads in the MultiheadAttention layers. - ff_dim (int): Dimension of the feedforward network in the Transformer blocks. - ffn_dropout_p (float): Dropout probability for the feedforward network. - attn_dropout_p (float): Dropout probability for the attention layers. - resid_dropout_p (float): Dropout probability for residual connections. - token_dropout_p (float): Dropout probability for token embeddings. - learn_te (bool): Whether to use learnable temporal embeddings. - """ - - def __init__(self, s1_bits, s2_bits, n_layers, d_model, n_heads, ff_dim, ffn_dropout_p, attn_dropout_p, resid_dropout_p, token_dropout_p, learn_te, news_dim=None): - super().__init__() - self.s1_bits = s1_bits - self.s2_bits = s2_bits - self.n_layers = n_layers - self.d_model = d_model - self.n_heads = n_heads - self.learn_te = learn_te - self.ff_dim = ff_dim - self.ffn_dropout_p = ffn_dropout_p - self.attn_dropout_p = attn_dropout_p - self.resid_dropout_p = resid_dropout_p - self.token_dropout_p = token_dropout_p - self.news_dim = news_dim - - self.s1_vocab_size = 2 ** self.s1_bits - self.token_drop = nn.Dropout(self.token_dropout_p) - self.embedding = HierarchicalEmbedding(self.s1_bits, self.s2_bits, self.d_model) - self.time_emb = TemporalEmbedding(self.d_model, self.learn_te) - self.transformer = nn.ModuleList([ - TransformerBlock(self.d_model, self.n_heads, self.ff_dim, self.ffn_dropout_p, self.attn_dropout_p, self.resid_dropout_p) - for _ in range(self.n_layers) - ]) - self.norm = RMSNorm(self.d_model) - self.dep_layer = DependencyAwareLayer(self.d_model) - self.head = DualHead(self.s1_bits, self.s2_bits, self.d_model) - - if self.news_dim is not None: - self.news_proj = nn.Linear(self.news_dim, self.d_model) - else: - self.news_proj = None - - self.apply(self._init_weights) - - def _init_weights(self, module): - - if isinstance(module, nn.Linear): - nn.init.xavier_normal_(module.weight) - if module.bias is not None: - nn.init.zeros_(module.bias) - elif isinstance(module, nn.Embedding): - nn.init.normal_(module.weight, mean=0, std=self.embedding.d_model ** -0.5) - elif isinstance(module, nn.LayerNorm): - nn.init.ones_(module.weight) - nn.init.zeros_(module.bias) - elif isinstance(module, RMSNorm): - nn.init.ones_(module.weight) - - def forward(self, s1_ids, s2_ids, stamp=None, padding_mask=None, use_teacher_forcing=False, s1_targets=None, news_emb=None): - """ - Args: - s1_ids (torch.Tensor): Input tensor of s1 token IDs. Shape: [batch_size, seq_len] - s2_ids (torch.Tensor): Input tensor of s2 token IDs. Shape: [batch_size, seq_len] - stamp (torch.Tensor, optional): Temporal stamp tensor. Shape: [batch_size, seq_len]. Defaults to None. - padding_mask (torch.Tensor, optional): Mask for padding tokens. Shape: [batch_size, seq_len]. Defaults to None. - use_teacher_forcing (bool, optional): Whether to use teacher forcing for s1 decoding. Defaults to False. - s1_targets (torch.Tensor, optional): Target s1 token IDs for teacher forcing. Shape: [batch_size, seq_len]. Defaults to None. - news_emb (torch.Tensor, optional): News embedding tensor. Shape: [batch_size, news_dim]. Defaults to None. - - Returns: - Tuple[torch.Tensor, torch.Tensor]: - - s1 logits: Logits for s1 token predictions. Shape: [batch_size, seq_len, s1_vocab_size] - - s2_logits: Logits for s2 token predictions, conditioned on s1. Shape: [batch_size, seq_len, s2_vocab_size] - """ - x = self.embedding([s1_ids, s2_ids]) - if stamp is not None: - time_embedding = self.time_emb(stamp) - x = x + time_embedding - x = self.token_drop(x) - - for layer in self.transformer: - x = layer(x, key_padding_mask=padding_mask) - - x = self.norm(x) - - if news_emb is not None and self.news_proj is not None: - news_bias = self.news_proj(news_emb).unsqueeze(1) # [B, 1, d_model] - x = x + news_bias - - s1_logits = self.head(x) - - if use_teacher_forcing: - sibling_embed = self.embedding.emb_s1(s1_targets) - else: - s1_probs = F.softmax(s1_logits.detach(), dim=-1) - sample_s1_ids = torch.multinomial(s1_probs.view(-1, self.s1_vocab_size), 1).view(s1_ids.shape) - sibling_embed = self.embedding.emb_s1(sample_s1_ids) - - x2 = self.dep_layer(x, sibling_embed, key_padding_mask=padding_mask) # Dependency Aware Layer: Condition on s1 embeddings - s2_logits = self.head.cond_forward(x2) - return s1_logits, s2_logits - - def decode_s1(self, s1_ids, s2_ids, stamp=None, padding_mask=None, news_emb=None): - """ - Decodes only the s1 tokens. - - This method performs a forward pass to predict only s1 tokens. It returns the s1 logits - and the context representation from the Transformer, which can be used for subsequent s2 decoding. - - Args: - s1_ids (torch.Tensor): Input tensor of s1 token IDs. Shape: [batch_size, seq_len] - s2_ids (torch.Tensor): Input tensor of s2 token IDs. Shape: [batch_size, seq_len] - stamp (torch.Tensor, optional): Temporal stamp tensor. Shape: [batch_size, seq_len]. Defaults to None. - padding_mask (torch.Tensor, optional): Mask for padding tokens. Shape: [batch_size, seq_len]. Defaults to None. - news_emb (torch.Tensor, optional): News embedding tensor. Shape: [batch_size, news_dim]. Defaults to None. - - Returns: - Tuple[torch.Tensor, torch.Tensor]: - - s1 logits: Logits for s1 token predictions. Shape: [batch_size, seq_len, s1_vocab_size] - - context: Context representation from the Transformer. Shape: [batch_size, seq_len, d_model] - """ - x = self.embedding([s1_ids, s2_ids]) - if stamp is not None: - time_embedding = self.time_emb(stamp) - x = x + time_embedding - x = self.token_drop(x) - - for layer in self.transformer: - x = layer(x, key_padding_mask=padding_mask) - - x = self.norm(x) - - if news_emb is not None and self.news_proj is not None: - news_bias = self.news_proj(news_emb).unsqueeze(1) # [B, 1, d_model] - x = x + news_bias - - s1_logits = self.head(x) - return s1_logits, x - - def decode_s2(self, context, s1_ids, padding_mask=None): - """ - Decodes the s2 tokens, conditioned on the context and s1 tokens. - - This method decodes s2 tokens based on a pre-computed context representation (typically from `decode_s1`) - and the s1 token IDs. It uses the dependency-aware layer and the conditional s2 head to predict s2 tokens. - - Args: - context (torch.Tensor): Context representation from the transformer (output of decode_s1). - Shape: [batch_size, seq_len, d_model] - s1_ids (torch.torch.Tensor): Input tensor of s1 token IDs. Shape: [batch_size, seq_len] - padding_mask (torch.Tensor, optional): Mask for padding tokens. Shape: [batch_size, seq_len]. Defaults to None. - - Returns: - torch.Tensor: s2 logits. Shape: [batch_size, seq_len, s2_vocab_size] - """ - sibling_embed = self.embedding.emb_s1(s1_ids) - x2 = self.dep_layer(context, sibling_embed, key_padding_mask=padding_mask) - return self.head.cond_forward(x2) - - -def top_k_top_p_filtering( - logits, - top_k: int = 0, - top_p: float = 1.0, - filter_value: float = -float("Inf"), - min_tokens_to_keep: int = 1, -): - """Filter a distribution of logits using top-k and/or nucleus (top-p) filtering - Args: - logits: logits distribution shape (batch size, vocabulary size) - if top_k > 0: keep only top k tokens with highest probability (top-k filtering). - if top_p < 1.0: keep the top tokens with cumulative probability >= top_p (nucleus filtering). - Nucleus filtering is described in Holtzman et al. (http://arxiv.org/abs/1904.09751) - Make sure we keep at least min_tokens_to_keep per batch example in the output - From: https://gist.github.com/thomwolf/1a5a29f6962089e871b94cbd09daf317 - """ - if top_k > 0: - top_k = min(max(top_k, min_tokens_to_keep), logits.size(-1)) # Safety check - # Remove all tokens with a probability less than the last token of the top-k - indices_to_remove = logits < torch.topk(logits, top_k)[0][..., -1, None] - logits[indices_to_remove] = filter_value - return logits - - if top_p < 1.0: - sorted_logits, sorted_indices = torch.sort(logits, descending=True) - cumulative_probs = torch.cumsum(F.softmax(sorted_logits, dim=-1), dim=-1) - - # Remove tokens with cumulative probability above the threshold (token with 0 are kept) - sorted_indices_to_remove = cumulative_probs > top_p - if min_tokens_to_keep > 1: - # Keep at least min_tokens_to_keep (set to min_tokens_to_keep-1 because we add the first one below) - sorted_indices_to_remove[..., :min_tokens_to_keep] = 0 - # Shift the indices to the right to keep also the first token above the threshold - sorted_indices_to_remove[..., 1:] = sorted_indices_to_remove[..., :-1].clone() - sorted_indices_to_remove[..., 0] = 0 - - # scatter sorted tensors to original indexing - indices_to_remove = sorted_indices_to_remove.scatter(1, sorted_indices, sorted_indices_to_remove) - logits[indices_to_remove] = filter_value - return logits - - -def sample_from_logits(logits, temperature=1.0, top_k=None, top_p=None, sample_logits=True): - logits = logits / temperature - if top_k is not None or top_p is not None: - if top_k > 0 or top_p < 1.0: - logits = top_k_top_p_filtering(logits, top_k=top_k, top_p=top_p) - - probs = F.softmax(logits, dim=-1) - - if not sample_logits: - _, x = top_k(probs, k=1, dim=-1) - else: - x = torch.multinomial(probs, num_samples=1) - - return x - - -def auto_regressive_inference(tokenizer, model, x, x_stamp, y_stamp, max_context, pred_len, clip=5, T=1.0, top_k=0, top_p=0.99, sample_count=5, verbose=False, news_emb=None): - with torch.no_grad(): - x = torch.clip(x, -clip, clip) - - device = x.device - x = x.unsqueeze(1).repeat(1, sample_count, 1, 1).reshape(-1, x.size(1), x.size(2)).to(device) - x_stamp = x_stamp.unsqueeze(1).repeat(1, sample_count, 1, 1).reshape(-1, x_stamp.size(1), x_stamp.size(2)).to(device) - y_stamp = y_stamp.unsqueeze(1).repeat(1, sample_count, 1, 1).reshape(-1, y_stamp.size(1), y_stamp.size(2)).to(device) - - x_token = tokenizer.encode(x, half=True) - - initial_seq_len = x.size(1) - batch_size = x_token[0].size(0) - total_seq_len = initial_seq_len + pred_len - full_stamp = torch.cat([x_stamp, y_stamp], dim=1) - - generated_pre = x_token[0].new_empty(batch_size, pred_len) - generated_post = x_token[1].new_empty(batch_size, pred_len) - - pre_buffer = x_token[0].new_zeros(batch_size, max_context) - post_buffer = x_token[1].new_zeros(batch_size, max_context) - buffer_len = min(initial_seq_len, max_context) - if buffer_len > 0: - start_idx = max(0, initial_seq_len - max_context) - pre_buffer[:, :buffer_len] = x_token[0][:, start_idx:start_idx + buffer_len] - post_buffer[:, :buffer_len] = x_token[1][:, start_idx:start_idx + buffer_len] - - if verbose: - ran = trange - else: - ran = range - for i in ran(pred_len): - current_seq_len = initial_seq_len + i - window_len = min(current_seq_len, max_context) - - if current_seq_len <= max_context: - input_tokens = [ - pre_buffer[:, :window_len], - post_buffer[:, :window_len] - ] - else: - input_tokens = [pre_buffer, post_buffer] - - context_end = current_seq_len - context_start = max(0, context_end - max_context) - current_stamp = full_stamp[:, context_start:context_end, :].contiguous() - - s1_logits, context = model.decode_s1(input_tokens[0], input_tokens[1], current_stamp, news_emb=news_emb) - s1_logits = s1_logits[:, -1, :] - sample_pre = sample_from_logits(s1_logits, temperature=T, top_k=top_k, top_p=top_p, sample_logits=True) - - s2_logits = model.decode_s2(context, sample_pre) - s2_logits = s2_logits[:, -1, :] - sample_post = sample_from_logits(s2_logits, temperature=T, top_k=top_k, top_p=top_p, sample_logits=True) - - generated_pre[:, i] = sample_pre.squeeze(-1) - generated_post[:, i] = sample_post.squeeze(-1) - - if current_seq_len < max_context: - pre_buffer[:, current_seq_len] = sample_pre.squeeze(-1) - post_buffer[:, current_seq_len] = sample_post.squeeze(-1) - else: - pre_buffer.copy_(torch.roll(pre_buffer, shifts=-1, dims=1)) - post_buffer.copy_(torch.roll(post_buffer, shifts=-1, dims=1)) - pre_buffer[:, -1] = sample_pre.squeeze(-1) - post_buffer[:, -1] = sample_post.squeeze(-1) - - full_pre = torch.cat([x_token[0], generated_pre], dim=1) - full_post = torch.cat([x_token[1], generated_post], dim=1) - - context_start = max(0, total_seq_len - max_context) - input_tokens = [ - full_pre[:, context_start:total_seq_len].contiguous(), - full_post[:, context_start:total_seq_len].contiguous() - ] - z = tokenizer.decode(input_tokens, half=True) - z = z.reshape(-1, sample_count, z.size(1), z.size(2)) - preds = z.cpu().numpy() - preds = np.mean(preds, axis=1) - - return preds - - -def calc_time_stamps(x_timestamp): - time_df = pd.DataFrame() - time_df['minute'] = x_timestamp.dt.minute - time_df['hour'] = x_timestamp.dt.hour - time_df['weekday'] = x_timestamp.dt.weekday - time_df['day'] = x_timestamp.dt.day - time_df['month'] = x_timestamp.dt.month - return time_df - - -class KronosPredictor: - - def __init__(self, model, tokenizer, device="cuda:0", max_context=512, clip=5): - self.tokenizer = tokenizer - self.model = model - self.max_context = max_context - self.clip = clip - self.price_cols = ['open', 'high', 'low', 'close'] - self.vol_col = 'volume' - self.amt_vol = 'amount' - self.time_cols = ['minute', 'hour', 'weekday', 'day', 'month'] - self.device = device - - self.tokenizer = self.tokenizer.to(self.device) - self.model = self.model.to(self.device) - - def generate(self, x, x_stamp, y_stamp, pred_len, T, top_k, top_p, sample_count, verbose, news_emb=None): - - x_tensor = torch.from_numpy(np.array(x).astype(np.float32)).to(self.device) - x_stamp_tensor = torch.from_numpy(np.array(x_stamp).astype(np.float32)).to(self.device) - y_stamp_tensor = torch.from_numpy(np.array(y_stamp).astype(np.float32)).to(self.device) - - preds = auto_regressive_inference(self.tokenizer, self.model, x_tensor, x_stamp_tensor, y_stamp_tensor, self.max_context, pred_len, - self.clip, T, top_k, top_p, sample_count, verbose, news_emb=news_emb) - preds = preds[:, -pred_len:, :] - return preds - - def predict(self, df, x_timestamp, y_timestamp, pred_len, T=1.0, top_k=0, top_p=0.9, sample_count=1, verbose=True, news_emb=None): - - if not isinstance(df, pd.DataFrame): - raise ValueError("Input must be a pandas DataFrame.") - - if not all(col in df.columns for col in self.price_cols): - raise ValueError(f"Price columns {self.price_cols} not found in DataFrame.") - - df = df.copy() - if self.vol_col not in df.columns: - df[self.vol_col] = 0.0 # Fill missing volume with zeros - df[self.amt_vol] = 0.0 # Fill missing amount with zeros - if self.amt_vol not in df.columns and self.vol_col in df.columns: - df[self.amt_vol] = df[self.vol_col] * df[self.price_cols].mean(axis=1) - - if df[self.price_cols + [self.vol_col, self.amt_vol]].isnull().values.any(): - raise ValueError("Input DataFrame contains NaN values in price or volume columns.") - - x_time_df = calc_time_stamps(x_timestamp) - y_time_df = calc_time_stamps(y_timestamp) - - x = df[self.price_cols + [self.vol_col, self.amt_vol]].values.astype(np.float32) - x_stamp = x_time_df.values.astype(np.float32) - y_stamp = y_time_df.values.astype(np.float32) - - x_mean, x_std = np.mean(x, axis=0), np.std(x, axis=0) - - x = (x - x_mean) / (x_std + 1e-5) - x = np.clip(x, -self.clip, self.clip) - - x = x[np.newaxis, :] - x_stamp = x_stamp[np.newaxis, :] - y_stamp = y_stamp[np.newaxis, :] - - if news_emb is not None: - news_emb_tensor = torch.from_numpy(np.array(news_emb).astype(np.float32)).to(self.device) - # Ensure batch dimension for news_emb if only one sample - if news_emb_tensor.ndim == 1: - news_emb_tensor = news_emb_tensor.unsqueeze(0) - else: - news_emb_tensor = None - - preds = self.generate(x, x_stamp, y_stamp, pred_len, T, top_k, top_p, sample_count, verbose, news_emb=news_emb_tensor) - - preds = preds.squeeze(0) - preds = preds * (x_std + 1e-5) + x_mean - - pred_df = pd.DataFrame(preds, columns=self.price_cols + [self.vol_col, self.amt_vol], index=y_timestamp) - return pred_df - - - def predict_batch(self, df_list, x_timestamp_list, y_timestamp_list, pred_len, T=1.0, top_k=0, top_p=0.9, sample_count=1, verbose=True): - """ - Perform parallel (batch) prediction on multiple time series. All series must have the same historical length and prediction length (pred_len). - - Args: - df_list (List[pd.DataFrame]): List of input DataFrames, each containing price columns and optional volume/amount columns. - x_timestamp_list (List[pd.DatetimeIndex or Series]): List of timestamps corresponding to historical data, length should match the number of rows in each DataFrame. - y_timestamp_list (List[pd.DatetimeIndex or Series]): List of future prediction timestamps, length should equal pred_len. - pred_len (int): Number of prediction steps. - T (float): Sampling temperature. - top_k (int): Top-k filtering threshold. - top_p (float): Top-p (nucleus sampling) threshold. - sample_count (int): Number of parallel samples per series, automatically averaged internally. - verbose (bool): Whether to display autoregressive progress. - - Returns: - List[pd.DataFrame]: List of prediction results in the same order as input, each DataFrame contains - `open, high, low, close, volume, amount` columns, indexed by corresponding `y_timestamp`. - """ - # Basic validation - if not isinstance(df_list, (list, tuple)) or not isinstance(x_timestamp_list, (list, tuple)) or not isinstance(y_timestamp_list, (list, tuple)): - raise ValueError("df_list, x_timestamp_list, y_timestamp_list must be list or tuple types.") - if not (len(df_list) == len(x_timestamp_list) == len(y_timestamp_list)): - raise ValueError("df_list, x_timestamp_list, y_timestamp_list must have consistent lengths.") - - num_series = len(df_list) - - x_list = [] - x_stamp_list = [] - y_stamp_list = [] - means = [] - stds = [] - seq_lens = [] - y_lens = [] - - for i in range(num_series): - df = df_list[i] - if not isinstance(df, pd.DataFrame): - raise ValueError(f"Input at index {i} is not a pandas DataFrame.") - if not all(col in df.columns for col in self.price_cols): - raise ValueError(f"DataFrame at index {i} is missing price columns {self.price_cols}.") - - df = df.copy() - if self.vol_col not in df.columns: - df[self.vol_col] = 0.0 - df[self.amt_vol] = 0.0 - if self.amt_vol not in df.columns and self.vol_col in df.columns: - df[self.amt_vol] = df[self.vol_col] * df[self.price_cols].mean(axis=1) - - if df[self.price_cols + [self.vol_col, self.amt_vol]].isnull().values.any(): - raise ValueError(f"DataFrame at index {i} contains NaN values in price or volume columns.") - - x_timestamp = x_timestamp_list[i] - y_timestamp = y_timestamp_list[i] - - x_time_df = calc_time_stamps(x_timestamp) - y_time_df = calc_time_stamps(y_timestamp) - - x = df[self.price_cols + [self.vol_col, self.amt_vol]].values.astype(np.float32) - x_stamp = x_time_df.values.astype(np.float32) - y_stamp = y_time_df.values.astype(np.float32) - - if x.shape[0] != x_stamp.shape[0]: - raise ValueError(f"Inconsistent lengths at index {i}: x has {x.shape[0]} vs x_stamp has {x_stamp.shape[0]}.") - if y_stamp.shape[0] != pred_len: - raise ValueError(f"y_timestamp length at index {i} should equal pred_len={pred_len}, got {y_stamp.shape[0]}.") - - x_mean, x_std = np.mean(x, axis=0), np.std(x, axis=0) - x_norm = (x - x_mean) / (x_std + 1e-5) - x_norm = np.clip(x_norm, -self.clip, self.clip) - - x_list.append(x_norm) - x_stamp_list.append(x_stamp) - y_stamp_list.append(y_stamp) - means.append(x_mean) - stds.append(x_std) - - seq_lens.append(x_norm.shape[0]) - y_lens.append(y_stamp.shape[0]) - - # Require all series to have consistent historical and prediction lengths for batch processing - if len(set(seq_lens)) != 1: - raise ValueError(f"Parallel prediction requires all series to have consistent historical lengths, got: {seq_lens}") - if len(set(y_lens)) != 1: - raise ValueError(f"Parallel prediction requires all series to have consistent prediction lengths, got: {y_lens}") - - x_batch = np.stack(x_list, axis=0).astype(np.float32) # (B, seq_len, feat) - x_stamp_batch = np.stack(x_stamp_list, axis=0).astype(np.float32) # (B, seq_len, time_feat) - y_stamp_batch = np.stack(y_stamp_list, axis=0).astype(np.float32) # (B, pred_len, time_feat) - - preds = self.generate(x_batch, x_stamp_batch, y_stamp_batch, pred_len, T, top_k, top_p, sample_count, verbose) - # preds: (B, pred_len, feat) - - pred_dfs = [] - for i in range(num_series): - preds_i = preds[i] * (stds[i] + 1e-5) + means[i] - pred_df = pd.DataFrame(preds_i, columns=self.price_cols + [self.vol_col, self.amt_vol], index=y_timestamp_list[i]) - pred_dfs.append(pred_df) - - return pred_dfs diff --git a/skills/alphaear-signal-tracker/scripts/utils/predictor/model/module.py b/skills/alphaear-signal-tracker/scripts/utils/predictor/model/module.py deleted file mode 100644 index 20b29b5..0000000 --- a/skills/alphaear-signal-tracker/scripts/utils/predictor/model/module.py +++ /dev/null @@ -1,562 +0,0 @@ -import math - -from einops import rearrange, reduce -import torch -import torch.nn as nn -from torch.autograd import Function -import torch.nn.functional as F - - -class DifferentiableEntropyFunction(Function): - @staticmethod - def forward(ctx, zq, basis, K, eps): - zb = (zq + 1) / 2 - zi = ((zb * basis).sum(-1)).to(torch.int64) - cnt = torch.scatter_reduce(torch.zeros(2 ** K, device=zq.device, dtype=zq.dtype), - 0, - zi.flatten(), - torch.ones_like(zi.flatten()).to(zq.dtype), - 'sum') - prob = (cnt + eps) / (cnt + eps).sum() - H = -(prob * torch.log(prob)).sum() - ctx.save_for_backward(zq, zi, prob) - ctx.K = K - return H - - @staticmethod - def backward(ctx, grad_output): - zq, zi, prob = ctx.saved_tensors - grad_array = -grad_output * (torch.log(prob) + 1) / zi.numel() / ctx.K - reord_grad = grad_array[zi.flatten()].reshape(zi.shape) - grad_input = reord_grad.unsqueeze(-1) * zq - return grad_input, None, None, None, None - - -def codebook_entropy(zq, basis, K, eps=1e-4): - return DifferentiableEntropyFunction.apply(zq, basis, K, eps) - - -class BinarySphericalQuantizer(nn.Module): - def __init__(self, embed_dim, beta, gamma0, gamma, zeta, - input_format='bchw', - soft_entropy=True, group_size=9, - persample_entropy_compute='analytical', - cb_entropy_compute='group', - l2_norm=True, - inv_temperature=1): - """ - Paper link: https://arxiv.org/pdf/2406.07548.pdf - Here we use the official implementation of the BinarySphericalQuantizer. - """ - super().__init__() - self.embed_dim = embed_dim - self.beta = beta # loss weight for commit loss - self.gamma0 = gamma0 # loss weight for entropy penalty - self.gamma = gamma # loss weight for entropy penalty - self.zeta = zeta # loss weight for entire entropy penalty - self.input_format = input_format - assert self.embed_dim % group_size == 0, "embed_dim must be divisible by group_size" - self.num_groups = self.embed_dim // group_size - self.group_size = group_size - assert persample_entropy_compute in ['group', 'analytical'], "persample_entropy_compute must be either 'group' or 'analytical'" - assert cb_entropy_compute in ['group', 'nce'], "cb_entropy_compute must be either 'group' or 'nce'" - self.persample_entropy_compute = persample_entropy_compute - self.cb_entropy_compute = cb_entropy_compute - self.l2_norm = l2_norm - self.inv_temperature = inv_temperature - - self.register_buffer('basis', 2 ** torch.arange(embed_dim - 1, -1, -1)) - self.register_buffer('group_basis', 2 ** torch.arange(group_size - 1, -1, -1)) - - self.num_dimensions = 2 ** embed_dim - self.bits_per_index = embed_dim - - # we only need to keep the codebook portion up to the group size - # because we approximate the H loss with this subcode - group_codes = torch.arange(2 ** self.group_size) - group_codebook = self.indexes_to_codes(group_codes).float()[:, -group_size:] - self.register_buffer('group_codebook', group_codebook, persistent=False) - - self.soft_entropy = soft_entropy # soft_entropy: Sec 3.2 of https://arxiv.org/pdf/1911.05894.pdf - - def quantize(self, z): - assert z.shape[-1] == self.embed_dim, f"Expected {self.embed_dim} dimensions, got {z.shape[-1]}" - - zhat = torch.where(z > 0, - torch.tensor(1, dtype=z.dtype, device=z.device), - torch.tensor(-1, dtype=z.dtype, device=z.device)) - return z + (zhat - z).detach() - - def forward(self, z, collect_metrics=True): - # if self.input_format == 'bchw': - # z = rearrange(z, 'b c h w -> b h w c') - zq = self.quantize(z) - - q_scale = 1. / (self.embed_dim ** 0.5) if self.l2_norm else 1. - - zq = zq * q_scale - - if not collect_metrics: - return zq, zq.new_zeros(()), {} - - indices = self.codes_to_indexes(zq.detach()) - group_indices = self.codes_to_group_indexes(zq.detach()) - if not self.training: - used_codes = torch.unique(indices, return_counts=False) - else: - used_codes = None - - if self.soft_entropy: - persample_entropy, cb_entropy, avg_prob = self.soft_entropy_loss(z) - entropy_penalty = self.gamma0 * persample_entropy - self.gamma * cb_entropy - else: - zb_by_sample = ((zq + 1) / 2).reshape(z.shape[0], -1, z.shape[-1]).to(torch.float32) - persample_entropy = self.get_hard_per_sample_entropy(zb_by_sample) - cb_entropy = codebook_entropy(zq, self.basis, self.embed_dim) - entropy_penalty = self.gamma0 * persample_entropy - self.gamma * cb_entropy - - # commit loss - commit_loss = self.beta * torch.mean(((zq.detach() - z) ** 2).sum(dim=-1)) - - # if self.input_format == 'bchw': - # zq = rearrange(zq, 'b h w c -> b c h w') - - return ( - zq, - commit_loss + self.zeta * entropy_penalty / self.inv_temperature, - {"H": cb_entropy, "used_codes": used_codes, "indices": indices, "group_indices": group_indices, - "avg_prob": avg_prob} - ) - - def soft_entropy_loss(self, z): - # if we divide the code in subgroups of size group_size, the codebook will be of size 2 ** group_size - # the sub-code is the last group_size bits of the full code - group_code_book = self.group_codebook / (self.embed_dim ** 0.5 if self.l2_norm else 1) - divided_z = rearrange(z, '... (g c) -> ... g c', c=self.group_size) - - # we calculate the distance between the divided_z and the codebook for each subgroup - distance = - 2 * torch.einsum('... g c, d c ->... g d', divided_z, group_code_book) - prob = (-distance * self.inv_temperature).softmax(dim=-1) - if self.persample_entropy_compute == 'analytical': - if self.l2_norm: - p = torch.sigmoid(-4 * z / (self.embed_dim ** 0.5) * self.inv_temperature) - else: - p = torch.sigmoid(-4 * z * self.inv_temperature) - prob = torch.stack([p, 1 - p], dim=-1) - per_sample_entropy = self.get_entropy(prob, dim=-1, normalize=False).sum(dim=-1).mean() - else: - per_sample_entropy = self.get_entropy(prob, dim=-1, normalize=False).sum(dim=-1).mean() - - # macro average of the probability of each subgroup - avg_prob = reduce(prob, '... g d ->g d', 'mean') - codebook_entropy = self.get_entropy(avg_prob, dim=-1, normalize=False) - - # the approximation of the entropy is the sum of the entropy of each subgroup - return per_sample_entropy, codebook_entropy.sum(), avg_prob - - def get_hard_per_sample_entropy(self, zb_by_sample): - probs_per_dim = zb_by_sample.sum(1) / zb_by_sample.shape[1] - persample_entropy = - probs_per_dim * torch.log(probs_per_dim + 1e-8) - (1 - probs_per_dim) * torch.log(1 - probs_per_dim + 1e-8) - persample_entropy = persample_entropy.sum(-1) - return persample_entropy.mean() - - def codes_to_indexes(self, zhat): - """Converts a `code` to an index in the codebook. - Args: - zhat: A tensor of shape (B, ..., C) containing the codes. must be in {-1, 1} - """ - assert zhat.shape[-1] == self.embed_dim, f"Expected {self.embed_dim} dimensions, got {zhat.shape[-1]}" - return ((zhat + 1) / 2 * self.basis).sum(axis=-1).to(torch.int64) - - def codes_to_group_indexes(self, zhat): - """Converts a `code` to a list of indexes (in groups) in the codebook. - Args: - zhat: A tensor of shape (B, ..., C) containing the codes. must be in {-1, 1} - """ - zhat_in_group = rearrange(zhat, 'b ... (g c) -> b ... g c', c=self.group_size) - return ((zhat_in_group + 1) / 2 * self.group_basis).sum(axis=-1).to(torch.int64) - - def indexes_to_codes(self, indices): - """Inverse of `indexes_to_codes`.""" - indices = indices.unsqueeze(-1) - codes_non_centered = torch.remainder( - torch.floor_divide(indices, self.basis), 2 - ) - return codes_non_centered * 2 - 1 - - def group_indexes_to_codes(self, group_indices): - """Inverse of `group_indexes_to_codes`.""" - group_indices = group_indices.unsqueeze(-1) - codes_non_centered = torch.remainder( - torch.floor_divide(group_indices, self.group_basis), 2 - ) - codes_non_centered = rearrange(codes_non_centered, 'b ... g c -> b ... (g c)') - return codes_non_centered * 2 - 1 - - def get_entropy(self, count, dim=-1, eps=1e-4, normalize=True): - if normalize: - probs = (count + eps) / (count + eps).sum(dim=dim, keepdim=True) - else: - probs = count - H = -(probs * torch.log(probs + 1e-8)).sum(dim=dim) - return H - - def get_group_codebook_entry(self, group_indices): - z_q = self.group_indexes_to_codes(group_indices) - q_scale = 1. / (self.embed_dim ** 0.5) if self.l2_norm else 1. - z_q = z_q * q_scale - if self.input_format == 'bchw': - h, w = int(z_q.shape[1] ** 0.5) - assert h * w == z_q.shape[1], 'Invalid sequence length' - z_q = rearrange(z_q, 'b (h w) c -> b c h w', h=h) - return z_q - - def get_codebook_entry(self, indices): - z_q = self.indexes_to_codes(indices) - q_scale = 1. / (self.embed_dim ** 0.5) if self.l2_norm else 1. - z_q = z_q * q_scale - if self.input_format == 'bchw': - h, w = int(z_q.shape[1] ** 0.5) - assert h * w == z_q.shape[1], 'Invalid sequence length' - z_q = rearrange(z_q, 'b (h w) c -> b c h w', h=h) - return z_q - - -class BSQuantizer(nn.Module): - - def __init__(self, s1_bits, s2_bits, beta, gamma0, gamma, zeta, group_size): - super().__init__() - self.codebook_dim = s1_bits + s2_bits - self.s1_bits = s1_bits - self.s2_bits = s2_bits - self.bsq = BinarySphericalQuantizer(self.codebook_dim, beta, gamma0, gamma, zeta, group_size=group_size) - - def bits_to_indices(self, bits): - bits = (bits >= 0).to(torch.long) - indices = 2 ** torch.arange( - 0, - bits.shape[-1], - 1, - dtype=torch.long, - device=bits.device, - ) - return (bits * indices).sum(-1) - - def forward(self, z, half=False, collect_metrics=True): - z = F.normalize(z, dim=-1) - quantized, bsq_loss, metrics = self.bsq(z, collect_metrics=collect_metrics) - if half: - q_pre = quantized[:, :, :self.s1_bits] - q_post = quantized[:, :, self.s1_bits:] - z_indices = [self.bits_to_indices(q_pre), self.bits_to_indices(q_post)] - else: - z_indices = self.bits_to_indices(quantized) - return bsq_loss, quantized, z_indices - - -class RMSNorm(torch.nn.Module): - def __init__(self, dim: int, eps: float = 1e-5): - super().__init__() - self.eps = eps - self.weight = nn.Parameter(torch.ones(dim)) - - def _norm(self, x): - return x * torch.rsqrt(torch.mean(x * x, dim=-1, keepdim=True) + self.eps) - - def forward(self, x): - output = self._norm(x.float()).type_as(x) - return output * self.weight - - -class FeedForward(nn.Module): - def __init__(self, d_model, ff_dim, ffn_dropout_p=0.0): - super().__init__() - - self.w1 = nn.Linear(d_model, ff_dim, bias=False) - self.w3 = nn.Linear(d_model, ff_dim, bias=False) - self.w2 = nn.Linear(ff_dim, d_model, bias=False) - self.ffn_dropout = nn.Dropout(ffn_dropout_p) - - def forward(self, x): - return self.ffn_dropout(self.w2(F.silu(self.w1(x)) * self.w3(x))) - - -class RotaryPositionalEmbedding(nn.Module): - def __init__(self, dim): - super().__init__() - inv_freq = 1.0 / (10000 ** (torch.arange(0, dim, 2).float() / dim)) - self.register_buffer("inv_freq", inv_freq) - self.seq_len_cached = None - self.cos_cached = None - self.sin_cached = None - - def _update_cos_sin_cache(self, x, seq_len): - if seq_len != self.seq_len_cached: - self.seq_len_cached = seq_len - t = torch.arange(seq_len, device=x.device).type_as(self.inv_freq) - freqs = torch.einsum('i,j->ij', t, self.inv_freq) - emb = torch.cat((freqs, freqs), dim=-1).to(x.device) - self.cos_cached = emb.cos()[None, None, :, :] - self.sin_cached = emb.sin()[None, None, :, :] - return self.cos_cached, self.sin_cached - - def forward(self, q, k): - cos, sin = self._update_cos_sin_cache(q, q.shape[-2]) - return ( - (q * cos) + (self._rotate_half(q) * sin), - (k * cos) + (self._rotate_half(k) * sin), - ) - - def _rotate_half(self, x): - x1, x2 = x.chunk(2, dim=-1) - return torch.cat((-x2, x1), dim=-1) - - -class MultiHeadAttentionWithRoPE(nn.Module): - def __init__(self, d_model, n_heads, attn_dropout_p=0.0, resid_dropout_p=0.0): - super().__init__() - self.d_model = d_model - self.n_heads = n_heads - self.head_dim = d_model // n_heads - - self.q_proj = nn.Linear(d_model, d_model) - self.k_proj = nn.Linear(d_model, d_model) - self.v_proj = nn.Linear(d_model, d_model) - self.out_proj = nn.Linear(d_model, d_model) - self.rotary = RotaryPositionalEmbedding(self.head_dim) - self.attn_dropout_p = attn_dropout_p - self.resid_dropout = nn.Dropout(resid_dropout_p) - - def forward(self, x, key_padding_mask=None): - batch_size, seq_len, _ = x.shape - - q = self.q_proj(x).view(batch_size, seq_len, self.n_heads, self.head_dim).transpose(1, 2) - k = self.k_proj(x).view(batch_size, seq_len, self.n_heads, self.head_dim).transpose(1, 2) - v = self.v_proj(x).view(batch_size, seq_len, self.n_heads, self.head_dim).transpose(1, 2) - - q, k = self.rotary(q, k) - - if key_padding_mask is not None: - attn_mask = key_padding_mask.unsqueeze(1).unsqueeze(2) # [batch, 1, 1, seq_len] - attn_mask = attn_mask.expand(-1, self.n_heads, seq_len, -1) # [batch, n_heads, q_len, k_len] - else: - attn_mask = None - - attn_output = F.scaled_dot_product_attention( - q, k, v, - attn_mask=attn_mask, - dropout_p=self.attn_dropout_p if self.training else 0.0, - is_causal=True - ) - - attn_output = attn_output.transpose(1, 2).contiguous().view(batch_size, seq_len, self.d_model) - return self.resid_dropout(self.out_proj(attn_output)) - - -class MultiHeadCrossAttentionWithRoPE(nn.Module): - def __init__(self, d_model, n_heads, attn_dropout_p=0.0, resid_dropout=0.0): - super().__init__() - self.d_model = d_model - self.n_heads = n_heads - self.head_dim = d_model // n_heads - - self.q_proj = nn.Linear(d_model, d_model) - self.k_proj = nn.Linear(d_model, d_model) - self.v_proj = nn.Linear(d_model, d_model) - self.out_proj = nn.Linear(d_model, d_model) - self.rotary = RotaryPositionalEmbedding(self.head_dim) - self.attn_dropout_p = attn_dropout_p - self.resid_dropout = nn.Dropout(resid_dropout) - - def forward(self, query, key, value, key_padding_mask=None): - batch_size, q_len, _ = query.shape - _, seq_len, _ = key.shape - - q = self.q_proj(query).view(batch_size, q_len, self.n_heads, self.head_dim).transpose(1, 2) - k = self.k_proj(key).view(batch_size, seq_len, self.n_heads, self.head_dim).transpose(1, 2) - v = self.v_proj(value).view(batch_size, seq_len, self.n_heads, self.head_dim).transpose(1, 2) - - q, k = self.rotary(q, k) - - if key_padding_mask is not None: - attn_mask = key_padding_mask.unsqueeze(1).unsqueeze(2) - attn_mask = attn_mask.expand(-1, self.n_heads, q_len, -1) - else: - attn_mask = None - - is_causal_flag = self.training - - attn_output = F.scaled_dot_product_attention( - q, k, v, - attn_mask=attn_mask, - dropout_p=self.attn_dropout_p if self.training else 0.0, - is_causal=is_causal_flag - ) - - attn_output = attn_output.transpose(1, 2).contiguous().view(batch_size, q_len, self.d_model) - return self.resid_dropout(self.out_proj(attn_output)) - - -class HierarchicalEmbedding(nn.Module): - def __init__(self, s1_bits, s2_bits, d_model=256): - super().__init__() - self.s1_bits = s1_bits - self.s2_bits = s2_bits - - vocab_s1 = 2 ** s1_bits - vocab_s2 = 2 ** s2_bits - - self.emb_s1 = nn.Embedding(vocab_s1, d_model) - self.emb_s2 = nn.Embedding(vocab_s2, d_model) - self.d_model = d_model - self.fusion_proj = nn.Linear(d_model * 2, d_model) - - nn.init.normal_(self.emb_s1.weight, mean=0, std=d_model ** -0.5) - nn.init.normal_(self.emb_s2.weight, mean=0, std=d_model ** -0.5) - - def split_token(self, token_ids: torch.Tensor, s2_bits: int): - """Inputs: - token_ids (torch.Tensor): Composite token IDs of shape [batch_size, seq_len] or [N], each in range [0, 2^(s1_bits + s2_bits) - 1]. - s2_bits (int): Number of low bits used for the fine token (s2). - """ - assert isinstance(s2_bits, int) and s2_bits > 0, "s2_bits must be a positive integer" - - t = token_ids.long() - mask = (1 << s2_bits) - 1 - s2_ids = t & mask # extract low bits - s1_ids = t >> s2_bits # extract high bits - return s1_ids, s2_ids - - def forward(self, token_ids): - """Inputs: - token_ids: - - tuple or list: (s1_ids, s2_ids), each of shape [batch_size, seq_len], or - - torch.Tensor: composite token IDs of shape [batch_size, seq_len], which will be split into (s1_ids, s2_ids) internally. - Output: [batch_size, seq_len, d_model] - """ - if isinstance(token_ids, tuple) or isinstance(token_ids, list): - s1_ids, s2_ids = token_ids - else: - s1_ids, s2_ids = self.split_token(token_ids, self.s2_bits) - s1_emb = self.emb_s1(s1_ids) * math.sqrt(self.d_model) - s2_emb = self.emb_s2(s2_ids) * math.sqrt(self.d_model) - return self.fusion_proj(torch.cat([s1_emb, s2_emb], dim=-1)) - - -class DependencyAwareLayer(nn.Module): - def __init__(self, d_model, n_heads=4, attn_dropout_p=0.0, resid_dropout=0.0): - super().__init__() - self.cross_attn = MultiHeadCrossAttentionWithRoPE(d_model, n_heads, attn_dropout_p, resid_dropout) - self.norm = RMSNorm(d_model) - - def forward(self, hidden_states, sibling_embed, key_padding_mask=None): - """hidden_states: [batch, seq_len, d_model] - sibling_embed: Embedding from another subtoken - """ - attn_out = self.cross_attn( - query=sibling_embed, - key=hidden_states, - value=hidden_states, - key_padding_mask=key_padding_mask - ) - return self.norm(hidden_states + attn_out) - - -class TransformerBlock(nn.Module): - def __init__(self, d_model, n_heads, ff_dim=1024, ffn_dropout_p=0.0, attn_dropout_p=0.0, resid_dropout_p=0.0): - super().__init__() - self.norm1 = RMSNorm(d_model) - self.self_attn = MultiHeadAttentionWithRoPE(d_model, n_heads, attn_dropout_p, resid_dropout_p) - self.norm2 = RMSNorm(d_model) - self.ffn = FeedForward(d_model, ff_dim, ffn_dropout_p) - - def forward(self, x, key_padding_mask=None): - residual = x - x = self.norm1(x) - attn_out = self.self_attn(x, key_padding_mask=key_padding_mask) - x = residual + attn_out - - residual = x - x = self.norm2(x) - ffn_out = self.ffn(x) - x = residual + ffn_out - return x - - -class DualHead(nn.Module): - def __init__(self, s1_bits, s2_bits, d_model): - super().__init__() - self.vocab_s1 = 2 ** s1_bits - self.vocab_s2 = 2 ** s2_bits - self.proj_s1 = nn.Linear(d_model, self.vocab_s1) - self.proj_s2 = nn.Linear(d_model, self.vocab_s2) - - def compute_loss(self, s1_logits, s2_logits, s1_targets, s2_targets, padding_mask=None): - if padding_mask is not None: - valid_mask = (padding_mask == 0) - s1_logits = s1_logits[valid_mask] - s2_logits = s2_logits[valid_mask] - s1_targets = s1_targets[valid_mask] - s2_targets = s2_targets[valid_mask] - ce_s1 = F.cross_entropy(s1_logits, s1_targets) - ce_s2 = F.cross_entropy(s2_logits, s2_targets) - else: - ce_s1 = F.cross_entropy(s1_logits.reshape(-1, self.vocab_s1), s1_targets.reshape(-1)) - ce_s2 = F.cross_entropy(s2_logits.reshape(-1, self.vocab_s2), s2_targets.reshape(-1)) - ce_loss = (ce_s1 + ce_s2) / 2 - return ce_loss, ce_s1, ce_s2 - - def forward(self, x): - return self.proj_s1(x) - - def cond_forward(self, x2): - return self.proj_s2(x2) - - -class FixedEmbedding(nn.Module): - def __init__(self, c_in, d_model): - super(FixedEmbedding, self).__init__() - - w = torch.zeros(c_in, d_model).float() - w.require_grad = False - - position = torch.arange(0, c_in).float().unsqueeze(1) - div_term = (torch.arange(0, d_model, 2).float() * -(math.log(10000.0) / d_model)).exp() - - w[:, 0::2] = torch.sin(position * div_term) - w[:, 1::2] = torch.cos(position * div_term) - - self.emb = nn.Embedding(c_in, d_model) - self.emb.weight = nn.Parameter(w, requires_grad=False) - - def forward(self, x): - return self.emb(x).detach() - - -class TemporalEmbedding(nn.Module): - def __init__(self, d_model, learn_pe): - super(TemporalEmbedding, self).__init__() - - minute_size = 60 - hour_size = 24 - weekday_size = 7 - day_size = 32 - month_size = 13 - - Embed = FixedEmbedding if not learn_pe else nn.Embedding - self.minute_embed = Embed(minute_size, d_model) - self.hour_embed = Embed(hour_size, d_model) - self.weekday_embed = Embed(weekday_size, d_model) - self.day_embed = Embed(day_size, d_model) - self.month_embed = Embed(month_size, d_model) - - def forward(self, x): - x = x.long() - - minute_x = self.minute_embed(x[:, :, 0]) - hour_x = self.hour_embed(x[:, :, 1]) - weekday_x = self.weekday_embed(x[:, :, 2]) - day_x = self.day_embed(x[:, :, 3]) - month_x = self.month_embed(x[:, :, 4]) - - return hour_x + weekday_x + day_x + month_x + minute_x \ No newline at end of file diff --git a/skills/alphaear-signal-tracker/scripts/utils/predictor/training.py b/skills/alphaear-signal-tracker/scripts/utils/predictor/training.py deleted file mode 100644 index 3b41724..0000000 --- a/skills/alphaear-signal-tracker/scripts/utils/predictor/training.py +++ /dev/null @@ -1,539 +0,0 @@ -import os -import sys -import time -import torch -import torch.nn as nn -import pandas as pd -import numpy as np -import json -import random -from loguru import logger -from datetime import datetime, timedelta -from sentence_transformers import SentenceTransformer -from skills._env_loader import load_unified_env - -load_unified_env() - -# Setup paths -KRONOS_DIR = os.path.dirname(os.path.abspath(__file__)) -SRC_DIR = os.path.dirname(os.path.dirname(KRONOS_DIR)) -if SRC_DIR not in sys.path: - sys.path.insert(0, SRC_DIR) - -from ..kronos.model import Kronos, KronosTokenizer, KronosPredictor -from ..database_manager import DatabaseManager -from ..stock_tools import StockTools -from ..search_tools import SearchTools -from ..llm.factory import get_model -from ..visualizer import VisualizerTools -from ..schema.models import ForecastResult, KLinePoint -from agno.agent import Agent - - -class AutoSynthesisTrainer: - def __init__(self, news_dim=384): - self.device = ( - "cuda" - if torch.cuda.is_available() - else "mps" - if torch.backends.mps.is_available() - else "cpu" - ) - self.db = DatabaseManager() - self.tools = StockTools(self.db) - self.searcher = SearchTools(self.db) - # Try loading from local cache first to avoid network timeouts - model_name = os.getenv( - "EMBEDDING_MODEL", "sentence-transformers/all-MiniLM-L6-v2" - ) - try: - logger.info(f"🔄 Attempting to load {model_name} from local cache...") - self.embedder = SentenceTransformer( - model_name, device=self.device, local_files_only=True - ) - logger.success("✅ Model loaded from local cache.") - except Exception: - logger.warning( - "⚠️ Local cache not found or incomplete. Attempting to download..." - ) - self.embedder = SentenceTransformer(model_name, device=self.device) - self.news_dim = news_dim - - # Try loading from local cache first to avoid network timeouts - try: - logger.info( - "🔄 Attempting to load Kronos and Tokenizer from local cache..." - ) - self.tokenizer = KronosTokenizer.from_pretrained( - "NeoQuasar/Kronos-Tokenizer-base", local_files_only=True - ).to(self.device) - base_model = Kronos.from_pretrained( - "NeoQuasar/Kronos-base", local_files_only=True - ) - logger.success("✅ Kronos and Tokenizer loaded from local cache.") - except Exception: - logger.warning( - "⚠️ Local Kronos/Tokenizer not found or incomplete. Attempting to download..." - ) - self.tokenizer = KronosTokenizer.from_pretrained( - "NeoQuasar/Kronos-Tokenizer-base" - ).to(self.device) - base_model = Kronos.from_pretrained("NeoQuasar/Kronos-base") - - self.model = Kronos( - base_model.s1_bits, - base_model.s2_bits, - base_model.n_layers, - base_model.d_model, - base_model.n_heads, - base_model.ff_dim, - base_model.ffn_dropout_p, - base_model.attn_dropout_p, - base_model.resid_dropout_p, - base_model.token_dropout_p, - base_model.learn_te, - news_dim=self.news_dim, - ).to(self.device) - self.model.load_state_dict(base_model.state_dict(), strict=False) - - # LLM for causality verification - provider = os.getenv("LLM_PROVIDER", "minimax") - model_id = os.getenv("LLM_MODEL", "Qwen") - self.llm_agent = Agent(model=get_model(provider, model_id)) - - def discover_shocks( - self, ticker_list, threshold=2.0, limit_per_stock=5, days=365, pred_len=5 - ): - """1. Find days with significant price movements (Look back 1 year)""" - shocks = [] - end_date = datetime.now().strftime("%Y-%m-%d") - start_date = (datetime.now() - timedelta(days=days)).strftime("%Y-%m-%d") - - for ticker in ticker_list: - df = self.tools.get_stock_price( - ticker, start_date=start_date, end_date=end_date - ) - if df.empty or len(df) < 60: - continue - - # Look for big moves - moves = df[df["change_pct"].abs() > threshold].copy() - if moves.empty: - continue - - count = 0 - for idx, row in moves.iterrows(): - # Ensure we have history before this day AND enough future days for eval - date_idx = df.index.get_loc(idx) - if date_idx < 50 or date_idx + pred_len > len(df): - continue - - shocks.append( - { - "ticker": ticker, - "date": row["date"], - "change": row["change_pct"], - "history": df.iloc[date_idx - 50 : date_idx], - "target": df.iloc[ - date_idx : date_idx + pred_len - ], # Now capturing pred_len days - } - ) - count += 1 - if count >= limit_per_stock: - break - - logger.info( - f"✨ Discovered {len(shocks)} potential price shocks over the last {days} days." - ) - return shocks - - def find_reason_and_verify(self, shock): - """2. Search for reasons and verify causality using LLM""" - ticker_info = self.db.get_stock_by_code(shock["ticker"]) - name = ticker_info["name"] if ticker_info else shock["ticker"] - date_str = shock["date"] - - # Try multiple query variations and engines - queries = [ - f"{name} ({shock['ticker']}) {date_str} 为什么涨跌 原因", - f"{name} {date_str} 异动 原因", - f"{shock['ticker']} {date_str} 新闻", - ] - - search_results = [] - for query in queries: - logger.info(f"🔍 Searching for reason: {query}") - # Try alternate engines - for engine in ["baidu"]: - try: - results = self.searcher.search_list( - query, engine=engine, max_results=3, enrich=False - ) - if results: - search_results = results - break - except Exception as e: - logger.warning(f"Search failed for {query} on {engine}: {e}") - - if search_results: - break - time.sleep(random.uniform(1.0, 2.0)) - - if not search_results: - logger.warning( - f"⚠️ No search results found for {name} on {date_str} after multiple attempts." - ) - return None - - context = "\n".join( - [f"- {r['title']}: {r.get('content', '')[:300]}" for r in search_results] - ) - - prompt = f""" - 任务:判断以下新闻是否解释了该股票在 {date_str} 的 {shock["change"]:.2f}% 价格变动。 - - 股票:{name} - 日期:{date_str} - 变动:{shock["change"]:.2f}% - - 搜索结果: - {context} - - 要求: - 1. 该新闻是否在该日期左右发生? - 2. 该新闻是否能逻辑上解释这种大幅波动(如财报、利好政策、重组、大环境暴跌等)? - 3. 如果是,请总结一段 100 字以内的“核心推动原因”。 - 4. 返回 JSON: {{"is_causal": true/false, "summary": "原因摘要"}} - """ - - try: - res = self.llm_agent.run(prompt) - data = json.loads( - res.content.replace("```json", "").replace("```", "").strip() - ) - if data.get("is_causal"): - logger.success( - f"✅ Verified cause for {name} on {date_str}: {data['summary']}" - ) - return data["summary"] - else: - logger.warning( - f"❌ Verified cause for {name} on {date_str}: {data['summary']}" - ) - return None - except Exception as e: - logger.warning(f"Verification failed: {e}") - return None - - def save_model(self, path=None): - """Save the news_proj weights""" - if path is None: - save_dir = os.path.join(SRC_DIR, "exports/models") - os.makedirs(save_dir, exist_ok=True) - path = os.path.join( - save_dir, f"kronos_news_v1_{datetime.now().strftime('%Y%m%d_%H%M')}.pt" - ) - - # We only really need to save the news_proj part as it's the only one we train - torch.save( - { - "news_proj_state_dict": self.model.news_proj.state_dict(), - "news_dim": self.news_dim, - "d_model": self.model.d_model, - }, - path, - ) - logger.success(f"💾 Model weights saved to {path}") - return path - - def run_synthesis_and_train(self, tickers, pred_len=5): - # 1. Discovery - shocks = self.discover_shocks(tickers, pred_len=pred_len) - print(f"find {len(shocks)} shocks") - - # 2. News Association & Verification - dataset = [] - max_news_items = 200 # Limit to 200 news items per session to avoid search bans - - logger.info( - f"🧬 Starting News Association for {len(shocks)} shocks (Max limit: {max_news_items})" - ) - - for i, shock in enumerate(shocks): - if len(dataset) >= max_news_items: - logger.info("Reached maximum news items limit for this session.") - break - - summary = self.find_reason_and_verify(shock) - if summary: - # 3. Embedding news - emb = self.embedder.encode(summary) - dataset.append( - { - "history": shock["history"], - "target": shock["target"], - "news_emb": emb, - "summary": summary, - } - ) - - # Add delay after search with randomness to avoid being blocked - if i < len(shocks) - 1: - delay = random.uniform(2.0, 4.0) - time.sleep(delay) - - if not dataset: - logger.error( - "❌ No verified news-price pairs found. Adjust threshold or check if news is available in that period." - ) - return - - # 4. Train/Val Split - random.seed(42) - random.shuffle(dataset) - - if len(dataset) < 2: - train_set = dataset - val_set = [] - logger.warning( - f"⚠️ Only {len(dataset)} sample(s) found. Training on all, skipping validation." - ) - else: - split_idx = max(1, int(len(dataset) * 0.8)) - if split_idx >= len(dataset): - split_idx = len(dataset) - 1 - - train_set = dataset[:split_idx] - val_set = dataset[split_idx:] - logger.info( - f"🏗️ Dataset Split: {len(train_set)} samples for training, {len(val_set)} for validation." - ) - - if not train_set: - logger.error("❌ No samples for training.") - return - - # 5. Training (Few-shot) - optimizer = torch.optim.Adam(self.model.news_proj.parameters(), lr=1e-3) - criterion = nn.CrossEntropyLoss() - self.model.train() - - loss_history = [] - logger.info(f"🚀 Training for 30 epochs...") - for epoch in range(30): - total_loss = 0 - for item in train_set: - optimizer.zero_grad() - - # Prep Data - hist_df = item["history"] - # For training, we still focus on the immediate next point (teacher forcing) - target_df = item["target"].iloc[:1] - - hist_raw = hist_df[ - ["open", "high", "low", "close", "volume"] - ].values.astype(np.float32) - hist_raw = np.column_stack([hist_raw, hist_raw[:, 3] * hist_raw[:, 4]]) - - mean, std = hist_raw.mean(axis=0), hist_raw.std(axis=0) + 1e-5 - hist_norm = ( - torch.from_numpy((hist_raw - mean) / std) - .unsqueeze(0) - .to(self.device) - ) - - target_raw = target_df[ - ["open", "high", "low", "close", "volume"] - ].values.astype(np.float32) - target_raw = np.column_stack( - [target_raw, target_raw[:, 3] * target_raw[:, 4]] - ) - target_norm = ( - torch.from_numpy((target_raw - mean) / std) - .unsqueeze(0) - .to(self.device) - ) - - with torch.no_grad(): - z_indices = self.tokenizer.encode(hist_norm, half=True) - t_indices = self.tokenizer.encode(target_norm, half=True) - s1_ids, s2_ids = z_indices[0], z_indices[1] - t_s1, t_s2 = t_indices[0], t_indices[1] - - news_t = torch.from_numpy(item["news_emb"]).unsqueeze(0).to(self.device) - s1_logits, s2_logits = self.model( - s1_ids, - s2_ids, - news_emb=news_t, - use_teacher_forcing=True, - s1_targets=t_s1, - ) - - loss = ( - criterion(s1_logits[:, -1, :], t_s1[:, 0]) - + criterion(s2_logits[:, -1, :], t_s2[:, 0]) - ) / 2 - loss.backward() - optimizer.step() - total_loss += loss.item() - - avg_epoch_loss = total_loss / max(1, len(train_set)) - loss_history.append(avg_epoch_loss) - - if (epoch + 1) % 10 == 0: - logger.info(f"Epoch {epoch + 1} Loss: {avg_epoch_loss:.4f}") - - # 5.1 Visualize Loss Curve - loss_chart = VisualizerTools.generate_loss_chart(loss_history) - VisualizerTools.render_chart_to_file( - loss_chart, - os.path.join(SRC_DIR, "exports/training_results/loss_curve.html"), - ) - - # 5.2 Save final model - self.save_model() - - # 6. Final Evaluation on Validation Set - if not val_set: - logger.warning("⚠️ Validation set is empty. Skipping statistical analysis.") - return - - logger.info( - f"🧪 Final Evaluation: Base vs News-Integrated ({pred_len}-day Window)" - ) - self.model.eval() - predictor = KronosPredictor(self.model, self.tokenizer, device=self.device) - - base_maes = [] - news_maes = [] - - print("\n" + "=" * 90) - print( - f"{'Date':<12} | {'Ticker':<8} | {'Base MAE':<15} | {'News MAE':<15} | {'Improvement'}" - ) - print("-" * 90) - - for item in val_set: - h = item["history"] - t = item["target"] - actuals = t["close"].values[:pred_len] - - x_ts = pd.to_datetime(h["date"]) - # Future timestamps: handle business days if possible, or just simple offset - future_dates = pd.date_range( - start=x_ts.iloc[-1] + timedelta(days=1), periods=pred_len, freq="B" - ) - y_ts = pd.Series(future_dates) - - # A. Base Prediction - p_base = predictor.predict( - h, x_ts, y_ts, pred_len=pred_len, news_emb=None, verbose=False - ) - b_preds = p_base["close"].values[: len(actuals)] - - # B. News-Aware Prediction - p_news = predictor.predict( - h, - x_ts, - y_ts, - pred_len=pred_len, - news_emb=item["news_emb"], - verbose=False, - ) - n_preds = p_news["close"].values[: len(actuals)] - - # Calculate MAE over the window - b_mae = np.mean(np.abs(b_preds - actuals)) - n_mae = np.mean(np.abs(n_preds - actuals)) - - base_maes.append(b_mae) - news_maes.append(n_mae) - - improvement = (b_mae - n_mae) / (b_mae + 1e-6) * 100 - - date_str = str(t["date"].values[0])[:10] - ticker = h.iloc[-1]["ticker"] if "ticker" in h.columns else "Stock" - print( - f"{date_str:<12} | {ticker:<8} | {b_mae:<15.4f} | {n_mae:<15.4f} | {improvement:>+7.1f}%" - ) - - # C. Generate Visualization for this case - try: - # Helper to convert DF to KLinePoints - def to_kp_list(preds_df): - points = [] - for idx, row in preds_df.iterrows(): - points.append( - KLinePoint( - date=str(idx)[:10], - open=row["open"], - high=row["high"], - low=row["low"], - close=row["close"], - volume=row["volume"] if "volume" in row else 0, - ) - ) - return points - - forecast_obj = ForecastResult( - ticker=ticker, - base_forecast=to_kp_list(p_base), - adjusted_forecast=to_kp_list(p_news), - rationale=item["summary"], - ) - - # Ground truth for visualizer expects a DataFrame with 'date' and 'close' - gt_df = t[["date", "open", "high", "low", "close", "volume"]] - - chart = VisualizerTools.generate_stock_chart( - df=h, - ticker=ticker, - title=f"Training Eval: {ticker} ({date_str}) Improvement: {improvement:.1f}%", - forecast=forecast_obj, - ground_truth=gt_df, - ) - - safe_date = date_str.replace("-", "") - filename = f"eval_{ticker}_{safe_date}.html" - VisualizerTools.render_chart_to_file( - chart, os.path.join(SRC_DIR, f"exports/training_results/{filename}") - ) - except Exception as e: - logger.error(f"Failed to generate eval chart for {ticker}: {e}") - - # Summary Statistics - avg_base_err = sum(base_maes) / max(1, len(base_maes)) - avg_news_err = sum(news_maes) / max(1, len(news_maes)) - overall_imp = (avg_base_err - avg_news_err) / (avg_base_err + 1e-6) * 100 - - print("-" * 90) - print( - f"{'AVERAGE':<12} | {'-':<8} | {avg_base_err:<15.4f} | {avg_news_err:<15.4f} | {overall_imp:>+7.1f}%" - ) - print("=" * 90 + "\n") - - logger.success( - f"🏁 Statistical Analysis Complete. Avg Error Reduction ({pred_len}-day): {overall_imp:.2f}%" - ) - logger.info( - f"📊 Visualization results saved to: {os.path.join(SRC_DIR, 'exports/training_results/')}" - ) - - -if __name__ == "__main__": - trainer = AutoSynthesisTrainer() - - logger.info("📂 Fetching all stock codes from database...") - res = trainer.db.execute_query("SELECT code FROM stock_list") - all_tickers = [row["code"] for row in res] - - if not all_tickers: - logger.warning("⚠️ No tickers found in stock_list table. Trying to sync...") - trainer.tools._check_and_update_stock_list(force=True) - res = trainer.db.execute_query("SELECT code FROM stock_list") - all_tickers = [row["code"] for row in res] - - logger.info(f"🚀 Starting training on potential stocks (1-year scan)...") - # 为了演示,我们扫描前 100 个股票,寻找最近一年的冲击点 - trainer.run_synthesis_and_train(all_tickers[:100], pred_len=1) diff --git a/skills/alphaear-signal-tracker/scripts/utils/search_tools.py b/skills/alphaear-signal-tracker/scripts/utils/search_tools.py deleted file mode 100644 index 50b08f3..0000000 --- a/skills/alphaear-signal-tracker/scripts/utils/search_tools.py +++ /dev/null @@ -1,611 +0,0 @@ -import os -import hashlib -import json -import re -import requests -import time -import threading -from typing import List, Dict, Optional, Any -from agno.tools.duckduckgo import DuckDuckGoTools -from agno.tools.baidusearch import BaiduSearchTools -from agno.agent import Agent -from loguru import logger -from datetime import datetime -from .database_manager import DatabaseManager -from .content_extractor import ContentExtractor -from .llm.factory import get_model -from .hybrid_search import LocalNewsSearch - -# 默认搜索缓存 TTL(秒),可通过环境变量覆盖 -DEFAULT_SEARCH_TTL = int(os.getenv("SEARCH_CACHE_TTL", "3600")) # 默认 1 小时 - - -class JinaSearchEngine: - """Jina Search API 封装 - 使用 s.jina.ai 进行网络搜索""" - - JINA_SEARCH_URL = "https://s.jina.ai/" - - # 速率限制配置 - _rate_limit_no_key = 10 # 无 key 时每分钟最大请求数 - _rate_window = 60.0 - _min_interval = 2.0 - _request_times = [] - _last_request_time = 0.0 - _lock = threading.Lock() - - def __init__(self): - self.api_key = os.getenv("JINA_API_KEY", "").strip() - self.has_api_key = bool(self.api_key) - if self.has_api_key: - logger.info("✅ Jina Search API key configured") - - @classmethod - def _wait_for_rate_limit(cls, has_api_key: bool) -> None: - """等待以满足速率限制""" - if has_api_key: - time.sleep(0.3) - return - - with cls._lock: - current_time = time.time() - cls._request_times = [t for t in cls._request_times if current_time - t < cls._rate_window] - - if len(cls._request_times) >= cls._rate_limit_no_key: - oldest = cls._request_times[0] - wait_time = cls._rate_window - (current_time - oldest) + 1.0 - if wait_time > 0: - logger.warning(f"⏳ Jina Search rate limit, waiting {wait_time:.1f}s...") - time.sleep(wait_time) - current_time = time.time() - cls._request_times = [t for t in cls._request_times if current_time - t < cls._rate_window] - - time_since_last = current_time - cls._last_request_time - if time_since_last < cls._min_interval: - time.sleep(cls._min_interval - time_since_last) - - cls._request_times.append(time.time()) - cls._last_request_time = time.time() - - def search(self, query: str, max_results: int = 5) -> List[Dict]: - """ - 使用 Jina Search API 执行搜索 - - Args: - query: 搜索关键词 - max_results: 返回结果数量 - - Returns: - 搜索结果列表,每个结果包含 title, url, content - """ - if not query: - return [] - - logger.info(f"🔍 Jina Search: {query}") - - # 等待速率限制 - self._wait_for_rate_limit(self.has_api_key) - - headers = { - "Accept": "application/json", - "X-Retain-Images": "none", - } - - if self.has_api_key: - headers["Authorization"] = f"Bearer {self.api_key}" - - try: - # Jina Search API: https://s.jina.ai/{query} - import urllib.parse - encoded_query = urllib.parse.quote(query) - url = f"{self.JINA_SEARCH_URL}{encoded_query}" - - response = requests.get(url, headers=headers, timeout=30) - - if response.status_code == 429: - logger.warning("⚠️ Jina Search rate limited (429), waiting 30s...") - time.sleep(30) - return self.search(query, max_results) - - if response.status_code != 200: - logger.warning(f"Jina Search failed (Status {response.status_code})") - return [] - - # 解析响应 - try: - data = response.json() - except json.JSONDecodeError: - # 如果返回纯文本,尝试解析 - data = {"data": [{"title": "Search Result", "url": "", "content": response.text}]} - - results = [] - - # Jina 返回格式可能是 {"data": [...]} 或直接是列表 - items = data.get("data", []) if isinstance(data, dict) else data - if not isinstance(items, list): - items = [items] if items else [] - - for i, item in enumerate(items[:max_results]): - if isinstance(item, dict): - results.append({ - "title": item.get("title", f"Result {i+1}"), - "url": item.get("url", ""), - "href": item.get("url", ""), # 兼容性 - "content": item.get("content", item.get("description", "")), - "body": item.get("content", item.get("description", "")), # 兼容性 - }) - elif isinstance(item, str): - results.append({ - "title": f"Result {i+1}", - "url": "", - "content": item - }) - - logger.info(f"✅ Jina Search returned {len(results)} results") - return results - - except requests.exceptions.Timeout: - logger.error("Jina Search timeout") - return [] - except requests.exceptions.RequestException as e: - logger.error(f"Jina Search request error: {e}") - return [] - except Exception as e: - logger.error(f"Jina Search unexpected error: {e}") - return [] - -class SearchTools: - """扩展性搜索工具库 - 支持多引擎聚合与内容缓存""" - - def __init__(self, db: DatabaseManager): - self.db = db - - # 检查 Jina API Key 是否配置 - jina_api_key = os.getenv("JINA_API_KEY", "").strip() - self._jina_enabled = bool(jina_api_key) - - self._engines = { - "ddg": DuckDuckGoTools(), - "baidu": BaiduSearchTools(), - "local": LocalNewsSearch(db) - } - - # 如果配置了 Jina API Key,添加 Jina 引擎 - if self._jina_enabled: - self._engines["jina"] = JinaSearchEngine() - logger.info("🚀 Jina Search engine enabled (JINA_API_KEY configured)") - - # 确定默认搜索引擎 - self._default_engine = "jina" if self._jina_enabled else "ddg" - - def _generate_hash(self, query: str, engine: str, max_results: int) -> str: - return hashlib.md5(f"{engine}:{query}:{max_results}".encode()).hexdigest() - - def search(self, query: str, engine: str = None, max_results: int = 5, ttl: Optional[int] = None) -> str: - """ - 使用指定搜索引擎执行网络搜索,结果会被缓存以提高效率。 - - Args: - query: 搜索关键词,如 "英伟达财报" 或 "光伏行业政策"。 - engine: 搜索引擎选择。可选值: - "jina" (Jina Search,需配置 JINA_API_KEY,LLM友好输出), - "ddg" (DuckDuckGo,推荐英文/国际搜索), - "baidu" (百度,推荐中文/国内搜索), - "local" (本地历史新闻搜索,基于向量+BM25)。 - 默认: 若配置了 JINA_API_KEY 则使用 "jina",否则 "ddg"。 - max_results: 期望返回的结果数量,默认 5 条。 - ttl: 缓存有效期(秒)。如果缓存超过此时间会重新搜索。 - 默认使用环境变量 SEARCH_CACHE_TTL 或 3600 秒。 - 设为 0 可强制刷新。 - - Returns: - 搜索结果的文本描述,包含标题、摘要和链接。 - """ - # 使用默认引擎(如果配置了 Jina 则优先使用 Jina) - if engine is None: - engine = self._default_engine - - if engine not in self._engines: - return f"Error: Unsupported engine '{engine}'. Available: {list(self._engines.keys())}" - - query_hash = self._generate_hash(query, engine, max_results) - effective_ttl = ttl if ttl is not None else DEFAULT_SEARCH_TTL - - # 1. 尝试从缓存读取 (local 引擎不缓存,因为它本身就是查库) - if engine != "local": - cache = self.db.get_search_cache(query_hash, ttl_seconds=effective_ttl if effective_ttl > 0 else None) - if cache and effective_ttl != 0: - logger.info(f"ℹ️ Found search results in cache for: {query} ({engine})") - return cache['results'] - - # 2. 执行真实搜索 - logger.info(f"📡 Searching {engine} for: {query}") - try: - tool = self._engines[engine] - if engine == "jina": - # Jina Search 返回 List[Dict] - jina_results = tool.search(query, max_results=max_results) - results = [] - for r in jina_results: - results.append({ - "title": r.get("title", ""), - "href": r.get("url", ""), - "body": r.get("content", "") - }) - elif engine == "ddg": - results = tool.duckduckgo_search(query, max_results=max_results) - elif engine == "baidu": - results = tool.baidu_search(query, max_results=max_results) - elif engine == "local": - # LocalNewsSearch 返回的是 List[Dict] - local_results = tool.search(query, top_n=max_results) - results = [] - for r in local_results: - results.append({ - "title": r.get("title"), - "href": r.get("url", "local"), - "body": r.get("content", "") - }) - else: - results = "Search not implemented for this engine." - - results_str = str(results) - if engine != "local": - self.db.save_search_cache(query_hash, query, engine, results_str) - return results_str - - except Exception as e: - # 搜索失败时的降级策略 - if engine == "jina": - logger.warning(f"⚠️ Jina search failed, falling back to ddg: {query} ({e})") - try: - return self.search(query, engine="ddg", max_results=max_results, ttl=ttl) - except Exception as e2: - logger.error(f"❌ DDG fallback also failed for {query}: {e2}") - elif engine == "ddg": - logger.warning(f"⚠️ DDG search failed, falling back to baidu: {query} ({e})") - try: - return self.search(query, engine="baidu", max_results=max_results, ttl=ttl) - except Exception as e2: - logger.error(f"❌ Baidu fallback also failed for {query}: {e2}") - - logger.error(f"❌ Search failed for {query}: {e}") - return f"Error occurred during search: {str(e)}" - - def search_list(self, query: str, engine: str = None, max_results: int = 5, ttl: Optional[int] = None, enrich: bool = True) -> List[Dict]: - """ - 执行搜索并返回结构化列表 (List[Dict])。 - Dict 包含: title, href (or url), body (or snippet) - - Args: - engine: 搜索引擎,默认使用配置的默认引擎(Jina 优先) - enrich: 是否抓取正文内容 (默认 True) - """ - # 使用默认引擎 - if engine is None: - engine = self._default_engine - - if engine not in self._engines: - logger.error(f"Unsupported engine {engine}") - return [] - - # 不同的 hash 以区分是否 enrichment - enrich_suffix = ":enriched" if enrich else "" - query_hash = self._generate_hash(query, engine + enrich_suffix, max_results) - effective_ttl = ttl if ttl is not None else DEFAULT_SEARCH_TTL - - # 1. 尝试从缓存读取 - cache = self.db.get_search_cache(query_hash, ttl_seconds=effective_ttl if effective_ttl > 0 else None) - if cache and effective_ttl != 0: - try: - cached_data = json.loads(cache['results']) - if isinstance(cached_data, list): - logger.info(f"ℹ️ Found structured search cache for: {query}") - return cached_data - except: - pass - - # 1.5 Smart Cache (Fuzzy + LLM) - if effective_ttl != 0: - try: - # 1. Similar cached queries - similar_queries = self.db.find_similar_queries(query, limit=3) - # Filter by TTL - valid_candidates = [] - for q in similar_queries: - if q['query'] == query: continue - q_time = datetime.fromisoformat(q['timestamp']) - if effective_ttl and (datetime.now() - q_time).total_seconds() > effective_ttl: - continue - q['type'] = 'cached_search' - valid_candidates.append(q) - - # 2. Relevant local news (as search results) - local_news = self.db.search_local_news(query, limit=3) - if local_news: - # Group local news as a single "candidate" source? Or individual? - # Better to treat "Local News Database" as one candidate source that contains X items. - # Or just add them to candidates list? - # Let's package strictly relevant news as a "local_news_bundle" - valid_candidates.append({ - 'type': 'local_news', - 'query': 'Local Database News', - 'items': local_news, - 'timestamp': datetime.now().isoformat() - }) - - if valid_candidates: - logger.info(f"🤔 Found {len(valid_candidates)} smart cache candidates (Queries/News). Asking LLM...") - evaluation = self._evaluate_cache_relevance(query, valid_candidates) - - if evaluation and evaluation.get('reuse', False): - idx = evaluation.get('index', -1) - if 0 <= idx < len(valid_candidates): - chosen = valid_candidates[idx] - logger.info(f"🤖 LLM suggested reusing: '{chosen.get('query')}' ({chosen['type']})") - - if chosen['type'] == 'cached_search': - # Load the chosen cache - cache = self.db.get_search_cache(chosen['query_hash']) - if cache: - try: - cached_data = json.loads(cache['results']) - if isinstance(cached_data, list): - return cached_data - except: - pass - elif chosen['type'] == 'local_news': - # Convert local news items to search result format - news_results = [] - for i, news in enumerate(chosen['items'], 1): - news_results.append({ - "id": news.get('id'), - "rank": i, - "title": news.get('title'), - "url": news.get('url'), - "content": news.get('content'), - "original_snippet": news.get('content')[:200] if news.get('content') else '', - "source": f"Local News ({news.get('source')})", - "publish_time": news.get('publish_time'), - "crawl_time": news.get('crawl_time'), - "sentiment_score": news.get('sentiment_score', 0), - "meta_data": {"origin": "local_db"} - }) - return news_results - - except Exception as e: - logger.warning(f"Smart cache check failed: {e}") - - # 2. 执行搜索 - logger.info(f"📡 Searching {engine} (structured) for: {query}") - try: - tool = self._engines[engine] - results = [] - if engine == "jina": - # Jina Search 直接返回结构化数据 - jina_results = tool.search(query, max_results=max_results) - for r in jina_results: - results.append({ - "title": r.get("title", ""), - "url": r.get("url", ""), - "href": r.get("url", ""), - "body": r.get("content", ""), - "content": r.get("content", ""), - "source": "Jina Search" - }) - elif engine == "ddg": - results = tool.duckduckgo_search(query, max_results=max_results) - elif engine == "baidu": - results = tool.baidu_search(query, max_results=max_results) - elif engine == "local": - # LocalNewsSearch 返回的是 List[Dict] - local_results = tool.search(query, top_n=max_results) - results = [] - for r in local_results: - results.append({ - "title": r.get("title"), - "url": r.get("url", "local"), - "body": r.get("content", "")[:500], - "source": f"Local ({r.get('source', 'db')})", - "publish_time": r.get("publish_time") - }) - - # 处理字符串类型的 JSON 返回 (Baidu 常返 JSON 字符串) - if isinstance(results, str) and engine not in ["local", "jina"]: - try: - results = json.loads(results) - except: - pass - - # 转为统一格式 - normalized_results = [] - if isinstance(results, list): - - for i, r in enumerate(results, 1): - title = r.get('title', '') - url = r.get('href') or r.get('url') or r.get('link', '') - content = r.get('body') or r.get('snippet') or r.get('abstract', '') - - if title and url: - normalized_results.append({ - "id": self._generate_hash(url + query, "search_item", i), - "rank": i, - "title": title, - "url": url, - "content": content, - "original_snippet": content, # 保留摘要 - "source": f"Search ({engine})", - "publish_time": datetime.now().isoformat(), # 暂用当前时间 - "crawl_time": datetime.now().isoformat(), - "meta_data": {"query": query, "engine": engine} - }) - - # Fallback if still string and failed to parse - elif isinstance(results, str) and results: - normalized_results.append({"title": query, "url": "", "content": results, "source": engine}) - - # 3. 抓取正文 & 计算情绪 (Enrichment) - # 注意:如果使用 Jina Search,内容已经是 LLM 友好格式,可选择跳过 enrichment - skip_content_enrichment = (engine == "jina") - - if enrich and normalized_results: - logger.info(f"🕸️ Enriching {len(normalized_results)} search results with Jina & Sentiment...") - extractor = ContentExtractor() - - # Lazy load sentiment tool - if not hasattr(self, 'sentiment_tool') or self.sentiment_tool is None: - from ..sentiment_tools import SentimentTools - self.sentiment_tool = SentimentTools(self.db) - - for item in normalized_results: - if item.get("url"): - try: - # 如果是 Jina Search,内容已经足够好,跳过额外抓取 - if skip_content_enrichment and item.get("content") and len(item.get("content", "")) > 100: - full_content = item["content"] - else: - # Use Jina Reader to get full content - full_content = extractor.extract_with_jina(item["url"], timeout=60) - - if full_content and len(full_content) > 100: - item["content"] = full_content - - # Calculate sentiment - # Use title + snippet of content for efficiency - text_to_analyze = f"{item['title']} {full_content[:500]}" - sent_result = self.sentiment_tool.analyze_sentiment(text_to_analyze) # Using self.sentiment_tool - score = sent_result.get('score', 0.0) - item["sentiment_score"] = float(score) - - logger.info(f" ✅ Enriched: {item['title'][:20]}... (Sentiment: {score:.2f})") - else: - # Fallback: Use snippet for sentiment - logger.info(f" ⚠️ Content short/failed for {item['url']}, using snippet for sentiment.") - text_to_analyze = f"{item['title']} {item['content']}" # content is snippet here - sent_result = self.sentiment_tool.analyze_sentiment(text_to_analyze) - score = sent_result.get('score', 0.0) - item["sentiment_score"] = float(score) - - except Exception as e: - # Fallback: Use snippet for sentiment on error - logger.warning(f"Failed to enrich {item['url']}: {e}. Using snippet.") - text_to_analyze = f"{item['title']} {item['content']}" - sent_result = self.sentiment_tool.analyze_sentiment(text_to_analyze) - score = sent_result.get('score', 0.0) - item["sentiment_score"] = float(score) - - # 缓存结果 list - if normalized_results: - # Pass list directly, DB manager will handle JSON dump for main cache and populate search_details - # Only cache if NOT from local news reuse (though this logic path is for fresh search) - self.db.save_search_cache(query_hash, query, engine, normalized_results) - - return normalized_results - - except Exception as e: - # 搜索失败时的降级策略 - if engine == "jina": - logger.warning(f"⚠️ Jina search_list failed, falling back to ddg: {query} ({e})") - try: - return self.search_list(query, engine="ddg", max_results=max_results, ttl=ttl, enrich=enrich) - except Exception as e2: - logger.error(f"❌ DDG fallback (search_list) also failed for {query}: {e2}") - elif engine == "ddg": - logger.warning(f"⚠️ DDG search_list failed, falling back to baidu: {query} ({e})") - try: - return self.search_list(query, engine="baidu", max_results=max_results, ttl=ttl, enrich=enrich) - except Exception as e2: - logger.error(f"❌ Baidu fallback (search_list) also failed for {query}: {e2}") - - logger.error(f"❌ Structured search failed for {query}: {e}") - return [] - - def _evaluate_cache_relevance(self, current_query: str, candidates: List[Dict]) -> Dict: - """ - 使用 LLM 评估缓存候选是否足以回答当前问题。 - """ - try: - # Prepare candidates text - candidates_desc = [] - for i, c in enumerate(candidates): - if c['type'] == 'cached_search': - # Preview cached results if available? - # Maybe just use the query string as a proxy for what's in there. - # Or peek at 'results' snippet. - preview = "" - try: - # Attempt to peek first result title from JSON string - # Note: c.get('results') might be a stringified JSON list - res_list = json.loads(c.get('results', '[]')) - if res_list and isinstance(res_list, list) and len(res_list) > 0: - first_item = res_list[0] - if isinstance(first_item, dict) and 'title' in first_item: - preview = f" (Contains: {first_item.get('title', '')[:50]}...)" - except: - pass - candidates_desc.append(f"[{i}] Old Search Query: '{c['query']}' {preview} (Time: {c['timestamp']})") - elif c['type'] == 'local_news': - # List titles of local news - titles = [item['title'] for item in c['items'][:3]] - candidates_desc.append(f"[{i}] Local Database News: {', '.join(titles)}... (Time: {c['timestamp']})") - - prompt = f""" - Task: Decide if existing information is sufficient for the new search query. - - New Query: "{current_query}" - - Available Information Candidates: - {chr(10).join(candidates_desc)} - - Instructions: - 1. Analyze if any candidate provides ENOUGH up-to-date info for the "New Query". - 2. If yes, choose the best one. - 3. If the query implies needing LATEST real-time info and candidates are old, choose none. - 4. Return strictly JSON: {{"reuse": true/false, "index": , "reason": "short explanation"}} - """ - # 初始化模型 - provider = os.getenv("LLM_PROVIDER", "minimax") - model_id = os.getenv("LLM_MODEL", "Qwen") - host = os.getenv("LLM_HOST") - if host: - model = get_model(provider, model_id, host=host) - else: - model = get_model(provider, model_id) - - agent = Agent(model=model, markdown=True) - - response = agent.run(prompt) - content = response.content - - # Parse JSON - json_match = re.search(r'```json\s*(.*?)\s*```', content, re.DOTALL) - if json_match: - return json.loads(json_match.group(1)) - elif '{' in content: - # Fallback for cases where LLM doesn't wrap in ```json - return json.loads(content[content.find('{'):content.rfind('}')+1]) - return {"reuse": False} - - except Exception as e: - logger.warning(f"LLM evaluation failed: {e}") - return {"reuse": False} - - def aggregate_search(self, query: str, engines: Optional[List[str]] = None, max_results: int = 5) -> str: - """ - 使用多个搜索引擎同时搜索并聚合结果,获得更全面的信息覆盖。 - - Args: - query: 搜索关键词。 - engines: 要使用的搜索引擎列表。可选值: ["ddg", "baidu"]。 - 默认同时使用 ddg 和 baidu。 - max_results: 每个引擎期望返回的结果数量。 - - Returns: - 聚合后的搜索结果,按引擎分组显示。 - """ - engines = engines or ["ddg", "baidu"] - aggregated_results = [] - for engine in engines: - res = self.search(query, engine=engine, max_results=max_results) - aggregated_results.append(f"--- Results from {engine.upper()} ---\n{res}") - - return "\n\n".join(aggregated_results) diff --git a/skills/alphaear-signal-tracker/scripts/utils/sentiment_tools.py b/skills/alphaear-signal-tracker/scripts/utils/sentiment_tools.py deleted file mode 100644 index f4278b5..0000000 --- a/skills/alphaear-signal-tracker/scripts/utils/sentiment_tools.py +++ /dev/null @@ -1,287 +0,0 @@ -import os -from typing import Dict, List, Union, Optional -import json -from loguru import logger -from agno.agent import Agent -from .llm.factory import get_model -from .database_manager import DatabaseManager - -# 从环境变量读取默认情绪分析模式 -DEFAULT_SENTIMENT_MODE = os.getenv("SENTIMENT_MODE", "auto") # auto, bert, llm - - -class SentimentTools: - """ - 情绪分析工具 - 支持 LLM 和 BERT 两种模式 - - 模式说明: - - "auto": 自动选择,优先使用 BERT(速度快),不可用时回退到 LLM - - "bert": 强制使用 BERT 模型(需要 transformers 库) - - "llm": 强制使用 LLM(更准确但较慢) - - 可通过环境变量 SENTIMENT_MODE 设置默认模式。 - """ - - def __init__( - self, - db: DatabaseManager, - mode: Optional[str] = None, - model_provider: str = "openai", - model_id: str = "gpt-4o", - ): - """ - 初始化情绪分析工具。 - - Args: - db: 数据库管理器实例 - mode: 分析模式,可选 "auto", "bert", "llm"。None 则使用环境变量默认值。 - model_provider: LLM 提供商,如 "openai", "ust", "deepseek" - model_id: 模型标识符 - """ - self.db = db - self.mode = mode or DEFAULT_SENTIMENT_MODE - self.llm_model = None - self.bert_pipeline = None - - # Initialize LLM - try: - provider = "minimax" if os.getenv("MINIMAX_API_KEY") else model_provider - m_id = ( - os.getenv("LLM_MODEL", "MiniMax-Text-01") - if provider == "minimax" - else model_id - ) - self.llm_model = get_model(provider, m_id) - except Exception as e: - logger.warning(f"LLM initialization skipped: {e}") - - # Initialize BERT if needed - if self.mode in ["bert", "auto"]: - try: - from transformers import ( - pipeline, - AutoTokenizer, - AutoModelForSequenceClassification, - ) - from transformers.utils import logging as transformers_logging - - transformers_logging.set_verbosity_error() # 减少冗余日志 - - bert_model = os.getenv( - "BERT_SENTIMENT_MODEL", - "uer/roberta-base-finetuned-chinanews-chinese", - ) - - # 优先使用本地缓存 - try: - tokenizer = AutoTokenizer.from_pretrained( - bert_model, local_files_only=True - ) - model = AutoModelForSequenceClassification.from_pretrained( - bert_model, local_files_only=True - ) - - self.bert_pipeline = pipeline( - "sentiment-analysis", - model=model, - tokenizer=tokenizer, - device=-1, - ) - logger.info( - f"✅ BERT pipeline loaded from local cache: {bert_model}" - ) - except (OSError, ValueError, ImportError): - # 本地没有,则从网络下载 - logger.info(f"📡 Downloading BERT model: {bert_model}...") - tokenizer = AutoTokenizer.from_pretrained(bert_model) - model = AutoModelForSequenceClassification.from_pretrained( - bert_model - ) - - self.bert_pipeline = pipeline( - "sentiment-analysis", - model=model, - tokenizer=tokenizer, - device=-1, - ) - logger.info( - f"✅ BERT Sentiment pipeline ({bert_model}) initialized." - ) - except ImportError: - logger.warning( - "Transformers library not installed. BERT sentiment analysis disabled." - ) - except Exception as e: - if self.mode == "bert": - logger.error(f"BERT mode requested but failed: {e}") - else: - logger.warning(f"BERT unavailable, using LLM only. Error: {e}") - self.bert_pipeline = None - - def analyze_sentiment(self, text: str) -> Dict[str, Union[float, str]]: - """ - 分析文本的情绪极性。根据初始化时的 mode 自动选择分析方法。 - - Args: - text: 需要分析的文本内容,如新闻标题或摘要。 - - Returns: - 包含以下字段的字典: - - score: 情绪分值,范围 -1.0(极度负面)到 1.0(极度正面),0.0 为中性 - - label: 情绪标签,"positive"/"negative"/"neutral" - - reason: 分析理由(仅 LLM 模式提供详细理由) - """ - if self.mode == "bert" and self.bert_pipeline: - results = self.analyze_sentiment_bert([text]) - return results[0] if results else {"score": 0.0, "label": "error"} - elif self.mode == "llm" or (self.mode == "auto" and not self.bert_pipeline): - return self.analyze_sentiment_llm(text) - else: - # auto mode with BERT available - results = self.analyze_sentiment_bert([text]) - return results[0] if results else {"score": 0.0, "label": "error"} - - def analyze_sentiment_llm(self, text: str) -> Dict[str, Union[float, str]]: - """ - 使用 LLM 进行深度情绪分析,可获得详细的分析理由。 - - Args: - text: 需要分析的文本,最多处理前 1000 字符。 - - Returns: - 包含 score, label, reason 的字典。 - """ - if not self.llm_model: - return {"score": 0.0, "label": "neutral", "error": "LLM not initialized"} - - analyzer = Agent(model=self.llm_model, markdown=True) - prompt = f"""请分析以下金融/新闻文本的情绪极性。 - 返回严格的 JSON 格式: - {{"score": , "label": "", "reason": "<简短理由>"}} - - 文本: {text[:1000]}""" - - try: - response = analyzer.run(prompt) - content = response.content - if "```json" in content: - content = content.split("```json")[1].split("```")[0].strip() - elif "```" in content: - content = content.split("```")[1].split("```")[0].strip() - return json.loads(content) - except Exception as e: - logger.error(f"LLM sentiment failed: {e}") - return {"score": 0.0, "label": "error", "reason": str(e)} - - def analyze_sentiment_bert(self, texts: List[str]) -> List[Dict]: - """ - 使用 BERT 进行批量高速情绪分析。 - - Args: - texts: 需要分析的文本列表。 - - Returns: - 与输入列表等长的分析结果列表。 - """ - if not self.bert_pipeline: - return [ - {"score": 0.0, "label": "error", "reason": "BERT not available"} - ] * len(texts) - - try: - results = self.bert_pipeline(texts, truncation=True, max_length=512) - processed = [] - for r in results: - label = r["label"].lower() - score = r["score"] - - # 标准化不同模型的标签格式 - if "negative" in label or "neg" in label: - score = -score - elif "neutral" in label or "neu" in label: - score = 0.0 - - processed.append( - { - "score": float(round(score, 3)), - "label": "positive" - if score > 0.1 - else ("negative" if score < -0.1 else "neutral"), - "reason": "BERT automated analysis", - } - ) - return processed - except Exception as e: - logger.error(f"BERT analysis failed: {e}") - return [{"score": 0.0, "label": "error", "reason": str(e)}] * len(texts) - - def batch_update_news_sentiment( - self, - source: Optional[str] = None, - limit: int = 50, - use_bert: Optional[bool] = None, - ): - """ - 批量更新数据库中新闻的情绪分数。 - - Args: - source: 筛选特定新闻源,如 "wallstreetcn"。None 则处理所有来源。 - limit: 最多处理的新闻数量。 - use_bert: 是否使用 BERT。None 则根据初始化模式自动决定。 - - Returns: - 成功更新的新闻数量。 - """ - news_items = self.db.get_daily_news(source=source, limit=limit) - to_analyze = [item for item in news_items if not item.get("sentiment_score")] - - if not to_analyze: - return 0 - - # 决定使用哪种方法 - should_use_bert = ( - use_bert - if use_bert is not None - else (self.bert_pipeline is not None and self.mode != "llm") - ) - - updated_count = 0 - cursor = self.db.conn.cursor() - - if should_use_bert and self.bert_pipeline: - logger.info( - f"🚀 Using BERT for batch analysis of {len(to_analyze)} items..." - ) - titles = [item["title"] for item in to_analyze] - results = self.analyze_sentiment_bert(titles) - - for item, analysis in zip(to_analyze, results): - cursor.execute( - """ - UPDATE daily_news - SET sentiment_score = ?, meta_data = json_set(COALESCE(meta_data, '{}'), '$.sentiment_reason', ?) - WHERE id = ? - """, - (analysis["score"], analysis["reason"], item["id"]), - ) - updated_count += 1 - else: - logger.info(f"🚶 Using LLM for analysis of {len(to_analyze)} items...") - for item in to_analyze: - analysis = self.analyze_sentiment_llm(item["title"]) - cursor.execute( - """ - UPDATE daily_news - SET sentiment_score = ?, meta_data = json_set(COALESCE(meta_data, '{}'), '$.sentiment_reason', ?) - WHERE id = ? - """, - ( - analysis.get("score", 0.0), - analysis.get("reason", ""), - item["id"], - ), - ) - updated_count += 1 - - self.db.conn.commit() - return updated_count diff --git a/skills/alphaear-signal-tracker/scripts/utils/stock_tools.py b/skills/alphaear-signal-tracker/scripts/utils/stock_tools.py deleted file mode 100644 index 5929f74..0000000 --- a/skills/alphaear-signal-tracker/scripts/utils/stock_tools.py +++ /dev/null @@ -1,257 +0,0 @@ -from datetime import datetime, timedelta -from typing import List, Dict, Optional -import akshare as ak -import pandas as pd -import re -import sqlite3 -from requests.exceptions import RequestException -from loguru import logger -from .database_manager import DatabaseManager -import os -from contextlib import contextmanager - -@contextmanager -def temporary_no_proxy(): - """Context manager to temporarily unset proxy environment variables.""" - proxies = {k: os.environ.get(k) for k in ['http_proxy', 'https_proxy', 'HTTP_PROXY', 'HTTPS_PROXY']} - for k in proxies: - if k in os.environ: - del os.environ[k] - try: - yield - finally: - for k, v in proxies.items(): - if v is not None: - os.environ[k] = v - -class StockTools: - """金融分析股票工具 - 结合高性能数据库缓存与增量更新""" - - def __init__(self, db: DatabaseManager, auto_update: bool = True): - """ - 初始化股票工具 - - Args: - db: 数据库管理器 - auto_update: 是否在列表为空时自动更新,默认 True - """ - self.db = db - if auto_update: - self._check_and_update_stock_list() - - def _check_and_update_stock_list(self, force: bool = False): - """检查并更新股票列表。仅在列表为空或 force=True 时从网络拉取。""" - # 直接查询表中记录数 - cursor = self.db.conn.cursor() - cursor.execute("SELECT COUNT(*) FROM stock_list") - count = cursor.fetchone()[0] - - if count > 0 and not force: - logger.info(f"ℹ️ Stock list already cached ({count} stocks)") - return - - logger.info("📡 Updating A-share and HK-share stock list from akshare...") - - def fetch_data(): - # A-share - df_a = ak.stock_zh_a_spot_em() - df_a = df_a[['代码', '名称']].copy() - df_a.columns = ['code', 'name'] - - # HK-share - df_hk = ak.stock_hk_spot_em() - df_hk = df_hk[['代码', '名称']].copy() - df_hk.columns = ['code', 'name'] - - # Combine - return pd.concat([df_a, df_hk], ignore_index=True) - - try: - try: - df_combined = fetch_data() - except (RequestException, Exception) as e: - if "Proxy" in str(e) or "proxy" in str(e): - logger.warning(f"⚠️ Proxy error detected: {e}. Retrying with proxy disabled...") - with temporary_no_proxy(): - df_combined = fetch_data() - else: - raise e - - self.db.save_stock_list(df_combined) - logger.info(f"✅ Cached {len(df_combined)} stocks (A-share + HK) to database.") - - except Exception as e: - logger.error(f"❌ Failed to sync stock list: {e}") - - - def search_ticker(self, query: str, limit: int = 5) -> List[Dict]: - """ - 模糊搜索 A 股股票代码或名称,支持常见缩写。 - """ - # 清洗后缀 (如 CATL.SZ -> CATL, 000001.SZ -> 000001) - clean_query = re.sub(r'\.(SZ|SH|HK|US)$', '', query, flags=re.IGNORECASE) - - # 常见缩写映射 - aliases = { - "CATL": "宁德时代", - "BYD": "比亚迪", - "TSLA": "特斯拉", - "Moutai": "贵州茅台", - "Tencent": "腾讯", - "Alibaba": "阿里巴巴", - "Meituan": "美团", - } - - search_query = aliases.get(clean_query.upper(), clean_query) - - # Robustness: if regex-like ticker code is embedded in query (e.g. "300364 中文在线"), try to extract it - if not search_query.isdigit(): - # Extract explicit 5-6 digit codes - match = re.search(r'\b(\d{5,6})\b', clean_query) - if match: - search_query = match.group(1) - - return self.db.search_stock(search_query, limit) - - def get_stock_price( - self, - ticker: str, - start_date: Optional[str] = None, - end_date: Optional[str] = None, - force_sync: bool = False, - ) -> pd.DataFrame: - """ - 获取指定股票的历史价格数据。优先从本地缓存读取,缺失时自动从网络补齐。 - - Args: - ticker: 股票代码,如 "600519"(贵州茅台)或 "000001"(平安银行)。 - start_date: 开始日期,格式 "YYYY-MM-DD"。默认为 90 天前。 - end_date: 结束日期,格式 "YYYY-MM-DD"。默认为今天。 - - Returns: - 包含 date, open, close, high, low, volume, change_pct 列的 DataFrame。 - """ - now = datetime.now() - if not end_date: - end_date = now.strftime('%Y-%m-%d') - if not start_date: - start_date = (now - timedelta(days=90)).strftime('%Y-%m-%d') - - df_db = self.db.get_stock_prices(ticker, start_date, end_date) - - need_update = False - if df_db.empty: - need_update = True - else: - db_latest = pd.to_datetime(df_db['date'].max()) - req_latest = pd.to_datetime(end_date) - if (req_latest - db_latest).days > 2: - need_update = True - - if force_sync: - need_update = True - - if need_update: - logger.info(f"📡 Data stale or missing for {ticker}, syncing from network...") - - # 清洗 ticker,确保只包含数字(Akshare A 股接口通常只需要数字代码) - clean_ticker = "".join(filter(str.isdigit, ticker)) - if not clean_ticker: - # Non A/H numeric tickers are not supported by the current data source. - logger.warning(f"⚠️ Unsupported ticker format (A/H only): {ticker}") - return df_db - - try: - s_fmt = start_date.replace("-", "") - e_fmt = end_date.replace("-", "") - - df_remote = None - - def fetch_data(): - if len(clean_ticker) == 5: - # HK Stock - return ak.stock_hk_hist( - symbol=clean_ticker, period="daily", - start_date=s_fmt, end_date=e_fmt, - adjust="qfq" - ) - else: - # A-share Stock - return ak.stock_zh_a_hist( - symbol=clean_ticker, period="daily", - start_date=s_fmt, end_date=e_fmt, - adjust="qfq" - ) - - try: - df_remote = fetch_data() - except (RequestException, Exception) as e: - if "Proxy" in str(e) or "proxy" in str(e): - logger.warning(f"⚠️ Proxy error detected: {e}. Retrying with proxy disabled...") - with temporary_no_proxy(): - df_remote = fetch_data() - else: - raise e - - if df_remote is not None and not df_remote.empty: - df_remote = df_remote.rename(columns={ - '日期': 'date', '开盘': 'open', '收盘': 'close', - '最高': 'high', '最低': 'low', '成交量': 'volume', - '涨跌幅': 'change_pct' - }) - # 确保日期格式正确 - df_remote['date'] = pd.to_datetime(df_remote['date']).dt.strftime('%Y-%m-%d') - - # 只有在获取到有意义的数据时才保存 - self.db.save_stock_prices(clean_ticker, df_remote) # 保存时使用清洗后的 clean_ticker - - # 重新查询数据库返回结果,保证一致性 - return self.db.get_stock_prices(clean_ticker, start_date, end_date) - else: - logger.warning(f"⚠️ Akshare returned empty data for {clean_ticker}") - - except KeyError as e: - # Akshare 有时在某些股票无数据时会抛出 KeyError - logger.warning(f"⚠️ Akshare data missing for {clean_ticker}: {e}") - except (RequestException, ConnectionError) as e: - logger.error(f"❌ Network error during Akshare sync for {clean_ticker}: {e}") - except sqlite3.Error as e: - logger.error(f"❌ Database error during Akshare sync for {clean_ticker}: {e}") - except Exception as e: - logger.error(f"❌ Unexpected error during Akshare sync for {clean_ticker}: {e}") - - return df_db - - -def get_stock_analysis(ticker: str, db: DatabaseManager) -> str: - """ - 生成指定股票的分析摘要报告。 - - Args: - ticker: 股票代码 - db: 数据库管理器实例 - - Returns: - Markdown 格式的分析报告,包含价格走势和关键指标。 - """ - tools = StockTools(db) - df = tools.get_stock_price(ticker) - - if df.empty: - return f"❌ 未能获取 {ticker} 的股价数据。" - - latest = df.iloc[-1] - change = ((latest['close'] - df.iloc[0]['close']) / df.iloc[0]['close']) * 100 - - report = [ - f"## 📊 {ticker} 分析报告", - f"- **查询时段**: {df.iloc[0]['date']} -> {latest['date']}", - f"- **当前价**: ¥{latest['close']:.2f}", - f"- **时段涨跌**: {change:+.2f}%", - f"- **最高/最低**: ¥{df['high'].max():.2f} / ¥{df['low'].min():.2f}", - "\n### 最近交易概览", - "```", - df.tail(5)[['date', 'close', 'change_pct', 'volume']].to_string(index=False), - "```" - ] - return "\n".join(report) diff --git a/skills/alphaear-signal-tracker/tests/test_tracker.py b/skills/alphaear-signal-tracker/tests/test_tracker.py deleted file mode 100644 index 7617ac4..0000000 --- a/skills/alphaear-signal-tracker/tests/test_tracker.py +++ /dev/null @@ -1,22 +0,0 @@ -import sys -import os -import unittest - -# Add skill root to path -sys.path.append(os.path.abspath(os.path.join(os.path.dirname(__file__), '..'))) - -try: - from scripts.fin_agent import FinAgent - from scripts.utils.database_manager import DatabaseManager -except ImportError as e: - print(f"Import Error: {e}") - sys.exit(1) - -class TestTracker(unittest.TestCase): - def test_init(self): - print("Testing FinAgent...") - # FinAgent Init might be complex. Checking import is a good start. - pass - -if __name__ == '__main__': - unittest.main() diff --git a/skills/alphaear-stock/SKILL.md b/skills/alphaear-stock/SKILL.md deleted file mode 100644 index bf2b582..0000000 --- a/skills/alphaear-stock/SKILL.md +++ /dev/null @@ -1,28 +0,0 @@ ---- -name: alphaear-stock -description: Search A-Share/HK/US finance stock tickers and retrieve finance stock price history. Use when user asks about finance stock codes, recent price changes, or specific company finance stock info. ---- - -# AlphaEar Stock Skill - -## Overview - -Search A-Share/HK/US stock tickers and retrieve historical price data (OHLCV). - -## Capabilities - -### 1. Stock Search & Data - -Use `scripts/stock_tools.py` via `StockTools`. - -- **Search**: `search_ticker(query)` - - Fuzzy search by code or name (e.g., "Moutai", "600519"). - - Returns: List of `{code, name}`. -- **Get Price**: `get_stock_price(ticker, start_date, end_date)` - - Returns DataFrame with OHLCV data. - - Dates format: "YYYY-MM-DD". - -## Dependencies - -- `pandas`, `requests`, `akshare`, `yfinance` -- `scripts/database_manager.py` (stock tables) diff --git a/skills/alphaear-stock/scripts/__init__.py b/skills/alphaear-stock/scripts/__init__.py deleted file mode 100644 index e69de29..0000000 diff --git a/skills/alphaear-stock/scripts/database_manager.py b/skills/alphaear-stock/scripts/database_manager.py deleted file mode 100644 index eb5d451..0000000 --- a/skills/alphaear-stock/scripts/database_manager.py +++ /dev/null @@ -1,119 +0,0 @@ -import sqlite3 -from pathlib import Path -from typing import List, Dict, Optional -import pandas as pd -from loguru import logger - -class DatabaseManager: - """ - AlphaEar Stock Database Manager - Reduced version for alphaear-stock skill - """ - - def __init__(self, db_path: str = "data/signal_flux.db"): - self.db_path = Path(db_path) - self.db_path.parent.mkdir(parents=True, exist_ok=True) - self.conn = sqlite3.connect(str(self.db_path), check_same_thread=False) - self.conn.row_factory = sqlite3.Row - self._init_db() - logger.debug(f"💾 Stock Database initialized at {self.db_path}") - - def _init_db(self): - """Initialize stock-related tables""" - cursor = self.conn.cursor() - - # Stock Prices Table - cursor.execute(""" - CREATE TABLE IF NOT EXISTS stock_prices ( - ticker TEXT, - date TEXT, - open REAL, - close REAL, - high REAL, - low REAL, - volume REAL, - change_pct REAL, - PRIMARY KEY (ticker, date) - ) - """) - - # Stock List Table - cursor.execute(""" - CREATE TABLE IF NOT EXISTS stock_list ( - code TEXT PRIMARY KEY, - name TEXT - ) - """) - - cursor.execute("CREATE INDEX IF NOT EXISTS idx_stock_prices_ticker_date ON stock_prices(ticker, date)") - self.conn.commit() - - # --- Stock Operations --- - - def save_stock_list(self, df: pd.DataFrame): - cursor = self.conn.cursor() - try: - cursor.execute("DELETE FROM stock_list") - data = df[['code', 'name']].to_dict('records') - cursor.executemany( - "INSERT INTO stock_list (code, name) VALUES (:code, :name)", - data - ) - self.conn.commit() - except Exception as e: - logger.error(f"Error saving stock list: {e}") - - def search_stock(self, query: str, limit: int = 5) -> List[Dict]: - cursor = self.conn.cursor() - wild = f"%{query}%" - cursor.execute(""" - SELECT code, name FROM stock_list - WHERE code LIKE ? OR name LIKE ? - LIMIT ? - """, (wild, wild, limit)) - return [dict(row) for row in cursor.fetchall()] - - def get_stock_by_code(self, code: str) -> Optional[Dict[str, str]]: - if not code: return None - clean = "".join([c for c in str(code).strip() if c.isdigit()]) - if not clean: return None - - cursor = self.conn.cursor() - cursor.execute("SELECT code, name FROM stock_list WHERE code = ? LIMIT 1", (clean,)) - row = cursor.fetchone() - return dict(row) if row else None - - def save_stock_prices(self, ticker: str, df: pd.DataFrame): - if df.empty: return - cursor = self.conn.cursor() - try: - for _, row in df.iterrows(): - cursor.execute(""" - INSERT OR REPLACE INTO stock_prices - (ticker, date, open, close, high, low, volume, change_pct) - VALUES (?, ?, ?, ?, ?, ?, ?, ?) - """, ( - ticker, row['date'], row['open'], row['close'], - row['high'], row['low'], row['volume'], row['change_pct'] - )) - self.conn.commit() - except Exception as e: - logger.error(f"Error saving prices for {ticker}: {e}") - - def get_stock_prices(self, ticker: str, start_date: str, end_date: str) -> pd.DataFrame: - cursor = self.conn.cursor() - cursor.execute(""" - SELECT * FROM stock_prices - WHERE ticker = ? AND date >= ? AND date <= ? - ORDER BY date - """, (ticker, start_date, end_date)) - - rows = cursor.fetchall() - if not rows: return pd.DataFrame() - - columns = ['ticker', 'date', 'open', 'close', 'high', 'low', 'volume', 'change_pct'] - return pd.DataFrame([dict(row) for row in rows], columns=columns) - - def close(self): - if self.conn: - self.conn.close() diff --git a/skills/alphaear-stock/scripts/stock_tools.py b/skills/alphaear-stock/scripts/stock_tools.py deleted file mode 100644 index bcb8636..0000000 --- a/skills/alphaear-stock/scripts/stock_tools.py +++ /dev/null @@ -1,419 +0,0 @@ -from datetime import datetime, timedelta -from typing import List, Dict, Optional -import akshare as ak -import yfinance as yf -import pandas as pd -import re -import sqlite3 -import requests as _requests -from requests.exceptions import RequestException -from loguru import logger -from .database_manager import DatabaseManager -import os -from contextlib import contextmanager - -class EastMoneyDirect: - """东方财富 HTTP 直接调用 —— 作为 akshare 的零依赖降级方案。 - - 仅使用 requests,无需 API Key,国内网络直连。 - """ - - KLINE_URL = "https://push2his.eastmoney.com/api/qt/stock/kline/get" - LIST_URL = "https://push2.eastmoney.com/api/qt/clist/get" - UT = "fa5fd1943c7b386f172d6893dbfba10b" - - @staticmethod - def _secid(ticker: str) -> str: - """将纯数字 ticker 转为东方财富 secid 格式。 - - A股: 6开头 -> 1.{ticker}(上交所) | 其他 -> 0.{ticker}(深交所) - 港股: 5位数字 -> 116.{ticker} - """ - if len(ticker) == 5: - return f"116.{ticker}" - if ticker.startswith(('6', '9')): - return f"1.{ticker}" - return f"0.{ticker}" - - @classmethod - def fetch_kline(cls, ticker: str, start_date: str, end_date: str) -> pd.DataFrame: - """获取 K 线数据,返回与 akshare 对齐的 DataFrame。 - - Args: - ticker: 纯数字股票代码 - start_date: YYYYMMDD - end_date: YYYYMMDD - """ - params = { - 'secid': cls._secid(ticker), - 'fields1': 'f1,f2,f3,f4,f5,f6', - 'fields2': 'f51,f52,f53,f54,f55,f56,f57,f58,f59,f60,f61', - 'klt': '101', # 日K - 'fqt': '1', # 前复权 - 'beg': start_date, - 'end': end_date, - 'lmt': '1000', - 'ut': cls.UT, - } - resp = _requests.get(cls.KLINE_URL, params=params, timeout=10) - resp.raise_for_status() - data = resp.json().get('data') - if not data or not data.get('klines'): - return pd.DataFrame() - - # kline 格式: "日期,开盘,收盘,最高,最低,成交量,成交额,振幅,涨跌幅,涨跌额,换手率" - rows = [k.split(',') for k in data['klines']] - df = pd.DataFrame(rows, columns=[ - '日期', '开盘', '收盘', '最高', '最低', '成交量', - '成交额', '振幅', '涨跌幅', '涨跌额', '换手率' - ]) - # 转为数值类型 - for col in ['开盘', '收盘', '最高', '最低', '成交量', '涨跌幅']: - df[col] = pd.to_numeric(df[col], errors='coerce') - - return df - - @classmethod - def fetch_stock_list(cls, market: str = 'a') -> pd.DataFrame: - """获取股票列表。 - - Args: - market: 'a' for A股, 'hk' for 港股 - """ - if market == 'a': - fs = 'm:0+t:6,m:0+t:80,m:1+t:2,m:1+t:23' - else: - fs = 'm:128+t:3,m:128+t:4,m:128+t:1,m:128+t:2' - - all_items = [] - page = 1 - while True: - params = { - 'pn': str(page), 'pz': '5000', 'po': '1', 'np': '1', - 'fltt': '2', 'invt': '2', 'fid': 'f12', - 'fs': fs, 'fields': 'f12,f14', - 'ut': cls.UT, - } - resp = _requests.get(cls.LIST_URL, params=params, timeout=15) - resp.raise_for_status() - data = resp.json().get('data', {}) - diff = data.get('diff', []) - if not diff: - break - for item in diff: - all_items.append({'code': item.get('f12', ''), 'name': item.get('f14', '')}) - total = data.get('total', 0) - if page * 5000 >= total: - break - page += 1 - - return pd.DataFrame(all_items) - - -@contextmanager -def temporary_no_proxy(): - """Context manager to temporarily unset proxy environment variables.""" - proxies = {k: os.environ.get(k) for k in ['http_proxy', 'https_proxy', 'HTTP_PROXY', 'HTTPS_PROXY']} - for k in proxies: - if k in os.environ: - del os.environ[k] - try: - yield - finally: - for k, v in proxies.items(): - if v is not None: - os.environ[k] = v - -class StockTools: - """金融分析股票工具 - 结合高性能数据库缓存与增量更新""" - - def __init__(self, db: DatabaseManager, auto_update: bool = True): - """ - 初始化股票工具 - - Args: - db: 数据库管理器 - auto_update: 是否在列表为空时自动更新,默认 True - """ - self.db = db - if auto_update: - self._check_and_update_stock_list() - - def _check_and_update_stock_list(self, force: bool = False): - """检查并更新股票列表。仅在列表为空或 force=True 时从网络拉取。""" - # 直接查询表中记录数 - cursor = self.db.conn.cursor() - cursor.execute("SELECT COUNT(*) FROM stock_list") - count = cursor.fetchone()[0] - - if count > 0 and not force: - logger.info(f"ℹ️ Stock list already cached ({count} stocks)") - return - - logger.info("📡 Updating A-share and HK-share stock list...") - - df_combined = None - - # === 主路径: akshare === - try: - def fetch_data_ak(): - df_a = ak.stock_zh_a_spot_em() - df_a = df_a[['代码', '名称']].copy() - df_a.columns = ['code', 'name'] - - df_hk = ak.stock_hk_spot_em() - df_hk = df_hk[['代码', '名称']].copy() - df_hk.columns = ['code', 'name'] - - return pd.concat([df_a, df_hk], ignore_index=True) - - try: - df_combined = fetch_data_ak() - except (RequestException, Exception) as e: - if "Proxy" in str(e) or "proxy" in str(e): - logger.warning(f"⚠️ Proxy error detected: {e}. Retrying with proxy disabled...") - with temporary_no_proxy(): - df_combined = fetch_data_ak() - else: - raise e - logger.info(f"✅ akshare: fetched {len(df_combined)} stocks.") - except Exception as e: - logger.warning(f"⚠️ akshare stock list failed: {e}. Trying EastMoney direct...") - - # === 降级路径: 东方财富直接 HTTP === - if df_combined is None or df_combined.empty: - try: - df_a = EastMoneyDirect.fetch_stock_list('a') - df_hk = EastMoneyDirect.fetch_stock_list('hk') - df_combined = pd.concat([df_a, df_hk], ignore_index=True) - logger.info(f"✅ EastMoney direct: fetched {len(df_combined)} stocks.") - except Exception as e2: - logger.error(f"❌ All stock list sources failed. akshare + EastMoney: {e2}") - return - - if df_combined is not None and not df_combined.empty: - self.db.save_stock_list(df_combined) - logger.info(f"✅ Cached {len(df_combined)} stocks to database.") - - - def search_ticker(self, query: str, limit: int = 5) -> List[Dict]: - """ - 模糊搜索 A 股股票代码或名称,支持常见缩写。 - """ - # 清洗后缀 (如 CATL.SZ -> CATL, 000001.SZ -> 000001) - clean_query = re.sub(r'\.(SZ|SH|HK|US)$', '', query, flags=re.IGNORECASE) - - # 常见缩写映射 - aliases = { - "CATL": "宁德时代", - "BYD": "比亚迪", - "TSLA": "特斯拉", - "Moutai": "贵州茅台", - "Tencent": "腾讯", - "Alibaba": "阿里巴巴", - "Meituan": "美团", - } - - search_query = aliases.get(clean_query.upper(), clean_query) - - # Robustness: if regex-like ticker code is embedded in query (e.g. "300364 中文在线"), try to extract it - if not search_query.isdigit(): - # Extract explicit 5-6 digit codes - match = re.search(r'\b(\d{5,6})\b', clean_query) - if match: - search_query = match.group(1) - - res = self.db.search_stock(search_query, limit) - if not res and search_query.isalpha(): - # Robustness: mock search hit for alphabetic US tickers - return [{"code": search_query.upper(), "name": search_query.upper()}] - return res - - def get_stock_price( - self, - ticker: str, - start_date: Optional[str] = None, - end_date: Optional[str] = None, - force_sync: bool = False, - ) -> pd.DataFrame: - """ - 获取指定股票的历史价格数据。优先从本地缓存读取,缺失时自动从网络补齐。 - - Args: - ticker: 股票代码,如 "600519"(贵州茅台)或 "000001"(平安银行)。 - start_date: 开始日期,格式 "YYYY-MM-DD"。默认为 90 天前。 - end_date: 结束日期,格式 "YYYY-MM-DD"。默认为今天。 - - Returns: - 包含 date, open, close, high, low, volume, change_pct 列的 DataFrame。 - """ - now = datetime.now() - if not end_date: - end_date = now.strftime('%Y-%m-%d') - if not start_date: - start_date = (now - timedelta(days=90)).strftime('%Y-%m-%d') - - df_db = self.db.get_stock_prices(ticker, start_date, end_date) - - need_update = False - if df_db.empty: - need_update = True - else: - db_latest = pd.to_datetime(df_db['date'].max()) - req_latest = pd.to_datetime(end_date) - if (req_latest - db_latest).days > 2: - need_update = True - - if force_sync: - need_update = True - - if need_update: - logger.info(f"📡 Data stale or missing for {ticker}, syncing from network...") - - is_us_stock = bool(re.search(r'[a-zA-Z]', ticker)) and not bool(re.search(r'\d{5,6}', ticker)) - - if is_us_stock: - clean_ticker = ticker.upper() - else: - # 清洗 ticker,确保只包含数字(Akshare A 股接口通常只需要数字代码) - clean_ticker = "".join(filter(str.isdigit, ticker)) - if not clean_ticker: - logger.warning(f"⚠️ Unsupported ticker format: {ticker}") - return df_db - - try: - s_fmt = start_date.replace("-", "") - e_fmt = end_date.replace("-", "") - - df_remote = None - - def fetch_data_akshare(): - """主路径: akshare""" - if is_us_stock: - return _fetch_data_yfinance() - if len(clean_ticker) == 5: - return ak.stock_hk_hist( - symbol=clean_ticker, period="daily", - start_date=s_fmt, end_date=e_fmt, - adjust="qfq" - ) - else: - return ak.stock_zh_a_hist( - symbol=clean_ticker, period="daily", - start_date=s_fmt, end_date=e_fmt, - adjust="qfq" - ) - - def _fetch_data_yfinance(): - """美股路径: yfinance""" - yf_ticker = yf.Ticker(clean_ticker) - end_dt = datetime.strptime(end_date, "%Y-%m-%d") + timedelta(days=1) - df_us = yf_ticker.history(start=start_date, end=end_dt.strftime("%Y-%m-%d")) - if df_us.empty: - return pd.DataFrame() - - df_us = df_us.reset_index() - date_col = 'Date' if 'Date' in df_us.columns else df_us.columns[0] - df_us = df_us.rename(columns={ - 'Open': 'open', 'Close': 'close', - 'High': 'high', 'Low': 'low', 'Volume': 'volume' - }) - - if pd.api.types.is_datetime64_any_dtype(df_us[date_col]): - df_us['date'] = df_us[date_col].dt.strftime('%Y-%m-%d') - else: - df_us['date'] = pd.to_datetime(df_us[date_col]).dt.strftime('%Y-%m-%d') - - df_us['change_pct'] = df_us['close'].pct_change() * 100 - df_us['change_pct'] = df_us['change_pct'].fillna(0) - - return df_us[['date', 'open', 'close', 'high', 'low', 'volume', 'change_pct']] - - def fetch_data_eastmoney(): - """降级路径: 东方财富直接 HTTP""" - logger.info(f"📡 Trying EastMoney direct for {clean_ticker}...") - return EastMoneyDirect.fetch_kline(clean_ticker, s_fmt, e_fmt) - - # === 多源尝试: akshare → 东方财富直接 === - try: - try: - df_remote = fetch_data_akshare() - except (RequestException, Exception) as e: - if "Proxy" in str(e) or "proxy" in str(e): - logger.warning(f"⚠️ Proxy error detected: {e}. Retrying with proxy disabled...") - with temporary_no_proxy(): - df_remote = fetch_data_akshare() - else: - raise e - except Exception as e: - logger.warning(f"⚠️ akshare failed for {clean_ticker}: {e}") - if not is_us_stock: - try: - df_remote = fetch_data_eastmoney() - except Exception as e2: - logger.warning(f"⚠️ EastMoney direct also failed for {clean_ticker}: {e2}") - raise e # 抛出原始错误 - - if df_remote is not None and not df_remote.empty: - if not is_us_stock: - df_remote = df_remote.rename(columns={ - '日期': 'date', '开盘': 'open', '收盘': 'close', - '最高': 'high', '最低': 'low', '成交量': 'volume', - '涨跌幅': 'change_pct' - }) - # 确保日期格式正确 - df_remote['date'] = pd.to_datetime(df_remote['date']).dt.strftime('%Y-%m-%d') - - # 只有在获取到有意义的数据时才保存 - self.db.save_stock_prices(clean_ticker, df_remote) # 保存时使用清洗后的 clean_ticker - - # 重新查询数据库返回结果,保证一致性 - return self.db.get_stock_prices(clean_ticker, start_date, end_date) - else: - logger.warning(f"⚠️ Akshare returned empty data for {clean_ticker}") - - except KeyError as e: - # Akshare 有时在某些股票无数据时会抛出 KeyError - logger.warning(f"⚠️ Akshare data missing for {clean_ticker}: {e}") - except (RequestException, ConnectionError) as e: - logger.error(f"❌ Network error during Akshare sync for {clean_ticker}: {e}") - except sqlite3.Error as e: - logger.error(f"❌ Database error during Akshare sync for {clean_ticker}: {e}") - except Exception as e: - logger.error(f"❌ Unexpected error during Akshare sync for {clean_ticker}: {e}") - - return df_db - - -def get_stock_analysis(ticker: str, db: DatabaseManager) -> str: - """ - 生成指定股票的分析摘要报告。 - - Args: - ticker: 股票代码 - db: 数据库管理器实例 - - Returns: - Markdown 格式的分析报告,包含价格走势和关键指标。 - """ - tools = StockTools(db) - df = tools.get_stock_price(ticker) - - if df.empty: - return f"❌ 未能获取 {ticker} 的股价数据。" - - latest = df.iloc[-1] - change = ((latest['close'] - df.iloc[0]['close']) / df.iloc[0]['close']) * 100 - - report = [ - f"## 📊 {ticker} 分析报告", - f"- **查询时段**: {df.iloc[0]['date']} -> {latest['date']}", - f"- **当前价**: ¥{latest['close']:.2f}", - f"- **时段涨跌**: {change:+.2f}%", - f"- **最高/最低**: ¥{df['high'].max():.2f} / ¥{df['low'].min():.2f}", - "\n### 最近交易概览", - "```", - df.tail(5)[['date', 'close', 'change_pct', 'volume']].to_string(index=False), - "```" - ] - return "\n".join(report) diff --git a/skills/alphaear-stock/tests/test_stock.py b/skills/alphaear-stock/tests/test_stock.py deleted file mode 100644 index 3f548df..0000000 --- a/skills/alphaear-stock/tests/test_stock.py +++ /dev/null @@ -1,24 +0,0 @@ -import sys -import os -import unittest - -# Add skill root to path -sys.path.append(os.path.abspath(os.path.join(os.path.dirname(__file__), '..'))) - -try: - from scripts.stock_tools import StockTools - from scripts.database_manager import DatabaseManager -except ImportError as e: - print(f"Import Error: {e}") - sys.exit(1) - -class TestStock(unittest.TestCase): - def test_init(self): - print("Testing StockTools Iteration...") - db = DatabaseManager(":memory:") - tools = StockTools(db) - self.assertIsNotNone(tools) - print("StockTools Initialized.") - -if __name__ == '__main__': - unittest.main() diff --git a/skills/ecommerce-astro/SKILL.md b/skills/ecommerce-astro/SKILL.md new file mode 100644 index 0000000..ae4730d --- /dev/null +++ b/skills/ecommerce-astro/SKILL.md @@ -0,0 +1,620 @@ +--- +name: ecommerce-astro +description: | + Full-featured e-commerce site builder with Astro 6, React, Supabase backend. + Creates online stores with optional multi-vendor marketplace, Thai language support, + inventory tracking, and order management. Use when: building e-commerce sites, + marketplaces, online stores, or Thai e-commerce stores. +--- + +# E-commerce Astro - E-commerce Site Builder + +**Category:** `fullstack` +**Tech Stack:** Astro 6 + React + Supabase + Tailwind v4 + +--- + +## 🎯 Purpose + +Create complete e-commerce websites with these core features: + +- ✅ **Product catalog** - Browse, filter, search products from Supabase +- ✅ **Inventory management** - Stock tracking with low-stock alerts +- ✅ **Order management** - Cart, checkout, order tracking, status updates +- ✅ **Thai language support** - Bilingual Thai/English with i18n routing +- ✅ **Review system** - Verified purchase reviews with ratings +- ✅ **Responsive design** - Mobile-first with React components + +### Optional Features (Enable/Disable) + +| Feature | Default | Description | +|---------|---------|-------------| +| `multi_vendor` | `false` | Multi-vendor marketplace with vendor dashboards | +| `payso_payment` | `false` | PaySo Thai payment gateway (stub for now) | +| `vendor_payouts` | `false` | Automated payout tracking (requires multi_vendor) | + +--- + +## 🚀 Quick Start + +```bash +# Generate e-commerce site (interactive mode) +python3 skills/ecommerce-astro/scripts/create_ecommerce.py \ + --name "My Store" \ + --output "./my-store" + +# With options +python3 skills/ecommerce-astro/scripts/create_ecommerce.py \ + --name "My Store" \ + --output "./my-store" \ + --multi-vendor true \ + --languages "th" +``` + +--- + +## 📋 Pre-Flight Questions + +Before running the script, gather these details: + +1. **Store Name:** (e.g., "Deal Plus Tech Store") +2. **Store Slug:** (e.g., "deal-plus-tech-store") +3. **Supabase Project URL:** From supabase.com dashboard +4. **Supabase Anon Key:** Public key for client-side +5. **Supabase Service Role Key:** For admin/server-side operations +6. **Multi-Vendor Mode:** Enable/disable vendor system (true/false) +7. **Languages:** Thai only (th), English only (en), or bilingual (th,en) + +--- + +## 📁 Generated Project Structure (Base) + +``` +store-name/ +├── astro.config.mjs +├── package.json +├── Dockerfile +├── docker-compose.yml +├── .env.example +├── .gitignore +│ +├── supabase/ +│ └── migrations/ +│ └── 001_initial_schema.sql +│ +├── src/ +│ ├── components/ +│ │ ├── cart/ +│ │ │ ├── CartBadge.tsx # Floating cart button +│ │ │ ├── CartButton.tsx # Header cart icon +│ │ │ ├── CartDrawer.tsx # Slide-out cart panel +│ │ │ ├── CartItems.tsx # Cart item list +│ │ │ └── CartSummary.tsx # Price breakdown +│ │ ├── checkout/ +│ │ │ └── CheckoutForm.tsx # Checkout form +│ │ ├── product/ +│ │ │ ├── ProductCard.astro # Product grid card +│ │ │ ├── ProductFilters.tsx # Category/price filters +│ │ │ ├── ProductGallery.tsx # Image gallery +│ │ │ ├── ProductVariants.tsx # Size/color variants +│ │ │ └── StockBadge.tsx # Inventory status +│ │ ├── review/ +│ │ │ ├── ReviewList.tsx # Product reviews +│ │ │ └── StarRating.tsx # Star rating display +│ │ └── layout/ +│ │ ├── Header.astro # Site header +│ │ └── Footer.astro # Site footer +│ │ +│ ├── layouts/ +│ │ └── Layout.astro # Base layout +│ │ +│ ├── lib/ +│ │ ├── supabase.ts # Supabase client (SSR-safe) +│ │ ├── auth.ts # JWT auth helpers +│ │ ├── utils.ts # Utility functions +│ │ └── types.ts # TypeScript types +│ │ +│ ├── stores/ +│ │ ├── cart.ts # Zustand cart (SSR-safe) +│ │ ├── auth.ts # Auth state +│ │ └── vendor.ts # Vendor state (if multi_vendor) +│ │ +│ ├── pages/ +│ │ ├── index.astro # Homepage +│ │ ├── products/ +│ │ │ ├── index.astro # Product listing +│ │ │ └── [slug].astro # Product detail +│ │ ├── cart.astro # Full cart page +│ │ ├── checkout.astro # Checkout page +│ │ ├── search.astro # Search page +│ │ ├── auth/ +│ │ │ ├── login.astro # Login page +│ │ │ └── register.astro # Register page +│ │ ├── account/ +│ │ │ ├── index.astro # Account dashboard +│ │ │ └── orders/ +│ │ │ ├── index.astro # Order history +│ │ │ └── [id].astro # Order detail +│ │ ├── vendor/ # Only if multi_vendor=true +│ │ │ ├── dashboard.astro # Vendor dashboard +│ │ │ ├── products/ +│ │ │ ├── orders.astro +│ │ │ └── settings.astro +│ │ ├── admin/ # Only if multi_vendor=true +│ │ │ ├── dashboard.astro +│ │ │ ├── vendors.astro +│ │ │ ├── users.astro +│ │ │ ├── orders.astro +│ │ │ └── categories.astro +│ │ └── api/ +│ │ ├── auth/ +│ │ ├── products/ +│ │ ├── orders/ +│ │ └── payments/ +│ │ +│ ├── i18n/ +│ │ ├── th.json # Thai translations +│ │ └── en.json # English translations +│ │ +│ └── styles/ +│ └── global.css # Global styles +│ +└── public/ + └── images/ +``` + +--- + +## 🗄️ Database Schema (Supabase PostgreSQL) + +The migration creates these tables. Schema can be customized per project. + +### Core Tables (Always Included) + +| Table | Purpose | +|-------|---------| +| `users` | Customer/admin accounts (id, email, password_hash, name, role, avatar_url) | +| `categories` | Product categories (hierarchical with parent_id) | +| `products` | Product catalog (id, vendor_id, category_id, name, slug, description, price, images JSONB, inventory, status, track_inventory, featured) | +| `reviews` | Product reviews (product_id, user_id, rating, comment, status) | +| `orders` | Customer orders (id, order_number, user_id, status, payment_status, total, shipping_address JSONB) | +| `order_items` | Line items per order (order_id, product_id, quantity, unit_price) | + +### Multi-Vendor Tables (Only if multi_vendor=true) + +| Table | Purpose | +|-------|---------| +| `vendor_profiles` | Store info (user_id, store_name, store_slug, store_description, status) | +| `product_variants` | Size/color variants (product_id, name, sku, price, inventory) | + +### Indexes + +```sql +-- Core indexes +CREATE INDEX idx_products_category ON products(category_id); +CREATE INDEX idx_products_vendor ON products(vendor_id); +CREATE INDEX idx_products_slug ON products(slug); +CREATE INDEX idx_products_status ON products(status); +CREATE INDEX idx_orders_user ON orders(user_id); +CREATE INDEX idx_orders_status ON orders(status); +CREATE INDEX idx_reviews_product ON reviews(product_id); +``` + +### Row Level Security (RLS) + +```sql +-- Products: Public read, vendor write own +-- Orders: User read own, vendor read own orders +-- Vendors: Admin manages vendor_profiles +``` + +### Key Lessons Learned + +1. **Images stored as JSONB** - Parse with `typeof images === 'string' ? JSON.parse(images) : images` +2. **Use service_role key for SSR** - Anonymous key blocked by RLS during server-side rendering +3. **Format:** `Authorization: Bearer {key}` header for Supabase REST API + +--- + +## 💳 Payment Integration + +Payment is stubbed by default. To enable real payments: + +1. Add PaySo credentials to `.env` +2. Create `/api/payments/create.ts` endpoint +3. Create `/api/webhooks/payso.ts` handler +4. Update `checkout.astro` to call payment API + +### Stub Implementation (Default) + +```typescript +// lib/payso.ts (stub) +export async function createPayment(order: Order) { + // TODO: Implement PaySo integration + console.log('Payment stub for order:', order.id); + return { success: true, paymentUrl: '/checkout/success' }; +} +``` + +--- + +## 🔐 Authentication + +### User Roles + +| Role | Permissions | +|------|-------------| +| `customer` | Browse, cart, checkout, orders | +| `vendor` | Products, orders (requires multi_vendor=true) | +| `admin` | All management (requires multi_vendor=true) | + +### Auth Flow + +1. Register with email/password +2. Login → JWT token stored in httpOnly cookie +3. Protected routes check session cookie +4. Role-based access for vendor/admin pages + +### SSR Authentication + +```typescript +// Check auth in Astro pages +const token = Astro.cookies.get('session')?.value; +if (!token) return Astro.redirect('/login'); +``` + +--- + +## 🛒 Cart & Checkout + +### Cart Features + +- **Floating Cart Button** - Fixed position bottom-right, blue circular button +- **Cart Drawer** - Slide-out panel with HeadlessUI +- **Persistent** - Zustand with localStorage (SSR-safe with getStorage) +- **Guest cart** - localStorage only +- **Logged-in cart** - Synced to database (optional) + +### Cart Store (SSR-Safe) + +```typescript +// stores/cart.ts +export const useCartStore = create()( + persist( + (set, get) => ({ ... }), + { + name: 'cart-storage', + partialize: (state) => ({ items: state.items }), + getStorage: () => { + if (typeof window === 'undefined') { + return { getItem: () => null, setItem: () => {}, removeItem: () => {} }; + } + return localStorage; + }, + } + ) +); +``` + +### Checkout Flow + +1. Cart Review → 2. Shipping Info → 3. Payment → 4. Confirmation + +--- + +## 📦 Vendor Dashboard (Only if multi_vendor=true) + +When `multi_vendor=true`, these pages are generated: + +### Vendor Features + +- **Dashboard** - Stats (products, orders, sales) +- **Products** - Add/edit/archive products +- **Orders** - View and manage orders +- **Settings** - Store profile + +### Vendor Onboarding Flow + +1. Register as customer +2. Apply for vendor status (`/vendors/apply`) +3. Admin approves → Vendor profile created +4. Access `/vendor/dashboard` + +### Hiding Vendor Pages + +When `multi_vendor=false`: +- No vendor registration link +- No `/vendor/*` routes +- No admin vendor management pages +- Products belong to "store" (no vendor_id) + +--- + +## 🌐 Internationalization + +### Thai/English Support + +- **URL Structure:** `/th/products`, `/en/products` (if bilingual) +- **Fallback:** Missing translation → English +- **Default:** Thai-only if `--languages th` + +### Translation Keys + +```json +// i18n/th.json +{ + "common": { + "addToCart": "เพิ่มลงตะกร้า", + "checkout": "ชำระเงิน", + "login": "เข้าสู่ระบบ" + }, + "product": { + "outOfStock": "สินค้าหมด", + "inStock": "มีสินค้า" + } +} +``` + +--- + +## 🔧 Environment Variables + +```bash +# Supabase (Required) +SUPABASE_URL=https://xxx.supabase.co +SUPABASE_ANON_KEY=eyJxxx # Public key (client-side) +SUPABASE_SERVICE_ROLE_KEY=eyJxxx # Admin key (server-side only!) + +# JWT (Required for auth) +JWT_SECRET=your-super-secret-jwt-key-min-32-chars + +# Site +SITE_URL=https://yourdomain.com +SITE_NAME=My Store + +# PaySo (Optional - stub by default) +PAYSOLO_MERCHANT_ID=your-merchant-id +PAYSOLO_API_KEY=your-api-key +PAYSOLO_SECRET_KEY=your-secret-key +PAYSOLO_CALLBACK_URL=https://yourdomain.com/api/webhooks/payso +``` + +--- + +## 🐳 Docker Deployment + +### Dockerfile (Astro SSR Mode) + +```dockerfile +FROM node:20-alpine +WORKDIR /app + +# Build-time env vars (needed for npm run build) +ENV PUBLIC_SUPABASE_URL=https://xxx.supabase.co +ENV PUBLIC_SUPABASE_ANON_KEY=eyJxxx +ENV SUPABASE_SERVICE_ROLE_KEY=eyJxxx +ENV SITE_URL=https://yourdomain.com +ENV JWT_SECRET=your-32-char-min-secret-key + +COPY package*.json ./ +RUN npm install +COPY . . +RUN npm run build + +EXPOSE 4321 +ENV HOST=0.0.0.0 +ENV PORT=4321 + +CMD ["npm", "run", "start"] +``` + +### docker-compose.yml + +```yaml +services: + web: + build: . + ports: + - "4321:4321" + env_file: + - .env +``` + +### Key Points + +1. **Build-time env vars** - Set `ENV` before `npm run build` so Astro can access them +2. **Service role key** - Only in Dockerfile ENV, not in client code +3. **Port 4321** - Astro default, map to 80 or your preferred port + +--- + +## 🚀 Deployment to Easypanel + +```bash +# 1. Generate site locally +python3 skills/ecommerce-astro/scripts/create_ecommerce.py \ + --name "my-store" \ + --output "./my-store" + +# 2. Push to Gitea +cd my-store +git init +git add . +git commit -m "Initial e-commerce site" +git remote add origin https://git.moreminimore.com/user/my-store.git +git push -u origin main + +# 3. Deploy to Easypanel +# Use easypanel-deploy skill or dashboard +``` + +--- + +## ✅ Success Criteria + +- [ ] Astro dev server runs without errors +- [ ] Supabase tables created successfully +- [ ] Products display from Supabase (images as JSONB) +- [ ] Cart adds/removes items (SSR-safe Zustand) +- [ ] Checkout creates order +- [ ] User registration/login works +- [ ] (If multi_vendor=true) Vendor dashboard accessible +- [ ] (If bilingual) Language switching works +- [ ] Docker build succeeds +- [ ] Deploys to Easypanel + +--- + +## 📚 Dependencies + +```json +{ + "astro": "^6.1.4", + "@astrojs/react": "^4.2.0", + "@astrojs/node": "^9.1.0", + "@astrojs/sitemap": "^3.2.0", + "@supabase/supabase-js": "^2.47.0", + "@supabase/ssr": "^0.6.1", + "@tailwindcss/vite": "^4.2.1", + "tailwindcss": "^4.2.1", + "zustand": "^5.0.0", + "react": "^19.0.0", + "react-dom": "^19.0.0", + "jose": "^6.0.0", + "@headlessui/react": "^2.0.0", + "lucide-react": "^0.400.0" +} +``` + +**Note:** Astro 6 is required for CSRF protection and other security features. + +--- + +## 🔗 Related Skills + +- **thai-frontend-dev** - Base Astro setup with PDPA compliance, cookie consent +- **easypanel-deploy** - Deploy to Easypanel +- **gitea-sync** - Sync code to Gitea + +--- + +## 📝 Example Usage + +### Single Vendor Store (Thai Only) + +```bash +python3 skills/ecommerce-astro/scripts/create_ecommerce.py \ + --name "ร้านค้าออนไลน์" \ + --slug "online-store" \ + --output "./thai-store" +# multi_vendor=false by default +``` + +### Multi-Vendor Marketplace (Bilingual) + +```bash +python3 skills/ecommerce-astro/scripts/create_ecommerce.py \ + --name "ThaiMart" \ + --slug "thaimart" \ + --multi-vendor true \ + --languages "th,en" \ + --output "./thaimart" +``` + +--- + +**Note:** After generation: +1. Run the Supabase migration in your dashboard +2. Update `.env` with your Supabase credentials +3. Add sample products to test + +--- + +## 🔧 Troubleshooting + +### SSR Error: `Cannot read properties of undefined (reading 'value')` + +**Cause:** Zustand persist middleware tries to access localStorage during SSR. + +**Fix:** Add `getStorage` to handle server-side: + +```typescript +getStorage: () => { + if (typeof window === 'undefined') { + return { getItem: () => null, setItem: () => {}, removeItem: () => {} }; + } + return localStorage; +} +``` + +### RLS Policy Blocks Read + +**Cause:** Anon key doesn't bypass RLS during SSR. + +**Fix:** Use service role key for server-side fetches: + +```typescript +headers: { + 'apikey': import.meta.env.SUPABASE_SERVICE_ROLE_KEY, + 'Authorization': `Bearer ${import.meta.env.SUPABASE_SERVICE_ROLE_KEY}` +} +``` + +### Images Show as `[` or Empty + +**Cause:** Images stored as JSONB string, not array. + +**Fix:** Parse before use: + +```typescript +const images = typeof product.images === 'string' + ? JSON.parse(product.images || '[]') + : (product.images || []); +``` + +### URLSearchParams Error + +**Cause:** Spread operator with undefined in template literal. + +**Fix:** Use string concatenation instead: + +```typescript +// Bad +href={`/products?${new URLSearchParams({...category && {category}})}`} + +// Good +href={`/products?sort=${sort}`} +``` + +### Cross-site POST form submissions are forbidden + +**Cause:** Astro 6 has built-in CSRF protection that blocks native form POST from different origins. + +**Fix:** Use client-side fetch instead of native form submission: + +```typescript +// In your .astro page +
+ + + +
+ + +``` diff --git a/skills/ecommerce-astro/scripts/.env.example b/skills/ecommerce-astro/scripts/.env.example new file mode 100644 index 0000000..e72ec3f --- /dev/null +++ b/skills/ecommerce-astro/scripts/.env.example @@ -0,0 +1,17 @@ +# Supabase Configuration +SUPABASE_URL=https://your-project.supabase.co +SUPABASE_ANON_KEY=your-anon-key +SUPABASE_SERVICE_ROLE_KEY=your-service-role-key + +# PaySo Payment Gateway (Thai Payment) +PAYSOLO_MERCHANT_ID=your-merchant-id +PAYSOLO_API_KEY=your-api-key +PAYSOLO_SECRET_KEY=your-secret-key +PAYSOLO_CALLBACK_URL=https://yourdomain.com/api/webhooks/payso + +# JWT Authentication +JWT_SECRET=your-super-secret-jwt-key-min-32-chars-here + +# Site Configuration +SITE_URL=https://yourdomain.com +SITE_NAME=My Store diff --git a/skills/ecommerce-astro/scripts/create_ecommerce.py b/skills/ecommerce-astro/scripts/create_ecommerce.py new file mode 100755 index 0000000..ee273ba --- /dev/null +++ b/skills/ecommerce-astro/scripts/create_ecommerce.py @@ -0,0 +1,2034 @@ +#!/usr/bin/env python3 +import os +import sys +import argparse +import shutil +from pathlib import Path + + +SCRIPT_DIR = Path(__file__).parent + + +def pkg_json(name): + return ( + '''{ + "name": "''' + + name + + """", + "type": "module", + "version": "1.0.0", + "scripts": { + "dev": "astro dev", + "build": "astro build", + "preview": "astro preview", + "astro": "astro" + }, + "dependencies": { + "astro": "^5.17.1", + "@astrojs/react": "^4.2.0", + "@astrojs/node": "^9.1.0", + "@astrojs/sitemap": "^3.2.0", + "@supabase/supabase-js": "^2.47.0", + "@supabase/ssr": "^0.6.1", + "@tailwindcss/vite": "^4.2.1", + "tailwindcss": "^4.2.1", + "zustand": "^5.0.0", + "react": "^19.0.0", + "react-dom": "^19.0.0", + "jose": "^6.0.0" + }, + "devDependencies": { + "@types/react": "^19.0.0", + "@types/react-dom": "^19.0.0", + "typescript": "^5.7.0" + } +}""" + ) + + +ASTRO_CONFIG = """import {{ defineConfig }} from 'astro/config'; +import react from '@astrojs/react'; +import node from '@astrojs/node'; +import sitemap from '@astrojs/sitemap'; +import tailwindcss from '@tailwindcss/vite'; + +export default defineConfig({{ + site: '{site_url}', + output: 'hybrid', + adapter: node({{ mode: 'standalone' }}), + i18n: {{ + locales: [{locales}], + defaultLocale: '{default_locale}', + routing: {{ + prefixDefaultLocale: false, + fallbackType: 'rewrite', + }}, + }}, + integrations: [ + react(), + sitemap({{ + i18n: {{ defaultLocale: '{default_locale}' }}, + }}), + ], + vite: {{ + plugins: [tailwindcss()], + }}, +}}); +""" + +TSCONFIG = """{ + "extends": "astro/tsconfigs/strict", + "compilerOptions": { + "jsx": "react-jsx", + "jsxImportSource": "react", + "baseUrl": ".", + "paths": { + "@/*": ["src/*"] + } + } +}""" + +SUPABASE_TS = """import {{ createClient }} from '@supabase/supabase-js'; +import type {{ Database }} from './types'; + +const supabaseUrl = import.meta.env.SUPABASE_URL; +const supabaseAnonKey = import.meta.env.SUPABASE_ANON_KEY; + +export const supabase = createClient(supabaseUrl, supabaseAnonKey); + +export type {{ Database }}; +""" + +AUTH_TS = r"""import { supabase } from './supabase'; +import { SignJWT, jwtVerify } from 'jose'; +import type { User, VendorProfile } from './types'; + +const JWT_SECRET = new TextEncoder().encode( + import.meta.env.JWT_SECRET || 'default-secret-change-me' +); + +export interface AuthUser { + id: string; + email: string; + name: string | null; + role: 'customer' | 'vendor' | 'admin'; + vendor_id?: string; +} + +export async function createSessionToken(user: AuthUser): Promise { + return new SignJWT({ + sub: user.id, + email: user.email, + name: user.name, + role: user.role, + vendor_id: user.vendor_id, + }) + .setProtectedHeader({ alg: 'HS256' }) + .setIssuedAt() + .setExpirationTime('7d') + .sign(JWT_SECRET); +} + +export async function verifySessionToken(token: string): Promise { + try { + const { payload } = await jwtVerify(token, JWT_SECRET); + return { + id: payload.sub as string, + email: payload.email as string, + name: payload.name as string | null, + role: payload.role as 'customer' | 'vendor' | 'admin', + vendor_id: payload.vendor_id as string | undefined, + }; + } catch { + return null; + } +} + +export async function registerUser( + email: string, + password: string, + name: string +): Promise<{ user: User | null; error: string | null }> { + const { data, error } = await supabase.auth.signUp({ + email, + password, + options: { + data: { name }, + }, + }); + + if (error) return { user: null, error: error.message }; + + const user = data.user; + if (!user) return { user: null, error: 'Registration failed' }; + + return { + user: { + id: user.id, + email: user.email || email, + name, + role: 'customer', + }, + error: null, + }; +} + +export async function loginUser( + email: string, + password: string +): Promise<{ user: AuthUser | null; token: string | null; error: string | null }> { + const { data, error } = await supabase.auth.signInWithPassword({ + email, + password, + }); + + if (error) return { user: null, token: null, error: error.message }; + + const user = data.user; + if (!user) return { user: null, token: null, error: 'Login failed' }; + + const { data: profileData } = await supabase + .from('vendor_profiles') + .select('id') + .eq('user_id', user.id) + .single(); + + const authUser: AuthUser = { + id: user.id, + email: user.email || email, + name: user.user_metadata?.name || null, + role: profileData ? 'vendor' : 'customer', + vendor_id: profileData?.id, + }; + + const token = await createSessionToken(authUser); + + return { user: authUser, token, error: null }; +} + +export async function logoutUser(): Promise { + await supabase.auth.signOut(); +} +""" + +PAYSOS_TS = r"""export interface PaySoPayment { + merchant_id: string; + order_id: string; + amount: number; + currency: string; + description: string; + callback_url: string; + return_url: string; + customer_name?: string; + customer_email?: string; + customer_phone?: string; +} + +export interface PaySoResponse { + code: string; + message: string; + data: { + payment_url: string; + qr_code?: string; + qr_image?: string; + transaction_id: string; + }; +} + +export interface PaySoWebhookPayload { + transaction_id: string; + order_id: string; + amount: number; + status: 'pending' | 'success' | 'failed'; + timestamp: string; + signature: string; +} + +export async function createPaySoPayment(payment: PaySoPayment): Promise { + const response = await fetch('https://api.paysogateway.com/v1/payment', { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + 'Authorization': `Bearer ${import.meta.env.PAYSOLO_API_KEY}`, + }, + body: JSON.stringify({ + merchant_id: import.meta.env.PAYSOLO_MERCHANT_ID, + order_id: payment.order_id, + amount: payment.amount, + currency: payment.currency || 'THB', + description: payment.description, + callback_url: payment.callback_url, + return_url: payment.return_url, + customer: { + name: payment.customer_name, + email: payment.customer_email, + phone: payment.customer_phone, + }, + }), + }); + + if (!response.ok) { + throw new Error(`PaySo API error: ${response.status}`); + } + + return response.json(); +} + +export async function verifyPaySoSignature( + payload: PaySoWebhookPayload, + signature: string +): Promise { + const crypto = await import('crypto'); + const secret = import.meta.env.PAYSOLO_SECRET_KEY; + const data = JSON.stringify({ + transaction_id: payload.transaction_id, + order_id: payload.order_id, + amount: payload.amount, + status: payload.status, + }); + const expectedSignature = crypto + .createHmac('sha256', secret) + .update(data) + .digest('hex'); + return signature === expectedSignature; +} +""" + +TYPES_TS = r"""export interface User { + id: string; + email: string; + name: string | null; + phone: string | null; + role: 'customer' | 'vendor' | 'admin'; + avatar_url: string | null; + created_at: string; + updated_at: string; +} + +export interface VendorProfile { + id: string; + user_id: string; + store_name: string; + store_slug: string; + store_description: string | null; + store_logo: string | null; + bank_account: string | null; + bank_name: string | null; + payout_status: 'pending' | 'approved' | 'rejected'; + total_earnings: number; + approved_at: string | null; + created_at: string; +} + +export interface Category { + id: string; + name: string; + slug: string; + description: string | null; + image_url: string | null; + parent_id: string | null; + sort_order: number; +} + +export interface Product { + id: string; + vendor_id: string; + category_id: string | null; + name: string; + slug: string; + description: string | null; + price: number; + compare_at_price: number | null; + cost_price: number | null; + sku: string | null; + barcode: string | null; + inventory: number; + low_stock_threshold: number; + track_inventory: boolean; + allow_backorder: boolean; + weight: number | null; + images: string[]; + metadata: Record; + status: 'draft' | 'active' | 'archived'; + featured: boolean; + created_at: string; + updated_at: string; +} + +export interface ProductVariant { + id: string; + product_id: string; + name: string; + sku: string | null; + price: number | null; + inventory: number; + attributes: Record; + image_url: string | null; +} + +export interface Review { + id: string; + product_id: string; + user_id: string; + order_id: string | null; + rating: number; + title: string | null; + comment: string | null; + images: string[]; + verified_purchase: boolean; + status: 'pending' | 'approved' | 'rejected'; + created_at: string; +} + +export interface Order { + id: string; + order_number: string; + user_id: string; + vendor_id: string | null; + status: 'pending' | 'confirmed' | 'processing' | 'shipped' | 'delivered' | 'cancelled' | 'refunded'; + payment_status: 'unpaid' | 'paid' | 'failed' | 'refunded'; + subtotal: number; + tax: number; + shipping_cost: number; + total: number; + currency: string; + payment_method: string | null; + payment_provider: string | null; + payment_ref: string | null; + shipping_name: string | null; + shipping_phone: string | null; + shipping_address: string | null; + shipping_city: string | null; + shipping_postal: string | null; + shipping_country: string; + notes: string | null; + created_at: string; + updated_at: string; +} + +export interface OrderItem { + id: string; + order_id: string; + product_id: string; + variant_id: string | null; + vendor_id: string | null; + quantity: number; + unit_price: number; + total_price: number; +} + +export interface CartItem { + id: string; + product: Product; + variant: ProductVariant | null; + quantity: number; +} +""" + +UTILS_TS = r"""export function formatPrice(amount: number, currency = 'THB'): string { + return new Intl.NumberFormat('th-TH', { + style: 'currency', + currency, + }).format(amount); +} + +export function generateSlug(text: string): string { + return text + .toLowerCase() + .replace(/[^\w\s-]/g, '') + .replace(/[\s_-]+/g, '-') + .replace(/^-+|-+$/g, ''); +} + +export function generateOrderNumber(): string { + const date = new Date(); + const dateStr = date.toISOString().slice(0, 10).replace(/-/g, ''); + const random = Math.random().toString(36).substring(2, 10).toUpperCase(); + return `ORD-${dateStr}-${random}`; +} + +export function cn(...classes: (string | undefined | null | false)[]): string { + return classes.filter(Boolean).join(' '); +} + +export function debounce any>( + fn: T, + delay: number +): (...args: Parameters) => void { + let timeoutId: ReturnType; + return (...args: Parameters) => { + clearTimeout(timeoutId); + timeoutId = setTimeout(() => fn(...args), delay); + }; +} +""" + +CART_STORE = r"""import { create } from 'zustand'; +import { persist } from 'zustand/middleware'; +import type { CartItem, Product, ProductVariant } from '../lib/types'; + +interface CartStore { + items: CartItem[]; + addItem: (product: Product, variant?: ProductVariant, quantity?: number) => void; + removeItem: (productId: string, variantId?: string) => void; + updateQuantity: (productId: string, variantId: string | undefined, quantity: number) => void; + clearCart: () => void; + getTotal: () => number; + getItemCount: () => number; +} + +export const useCartStore = create()( + persist( + (set, get) => ({ + items: [], + addItem: (product, variant, quantity = 1) => { + set((state) => { + const existingIndex = state.items.findIndex( + (item) => + item.product.id === product.id && + item.variant?.id === variant?.id + ); + + if (existingIndex >= 0) { + const newItems = [...state.items]; + newItems[existingIndex].quantity += quantity; + return { items: newItems }; + } + + return { + items: [ + ...state.items, + { id: crypto.randomUUID(), product, variant: variant || null, quantity }, + ], + }; + }); + }, + removeItem: (productId, variantId) => { + set((state) => ({ + items: state.items.filter( + (item) => + !(item.product.id === productId && item.variant?.id === variantId) + ), + })); + }, + updateQuantity: (productId, variantId, quantity) => { + if (quantity <= 0) { + get().removeItem(productId, variantId); + return; + } + set((state) => ({ + items: state.items.map((item) => + item.product.id === productId && item.variant?.id === variantId + ? { ...item, quantity } + : item + ), + })); + }, + clearCart: () => set({ items: [] }), + getTotal: () => { + return get().items.reduce( + (total, item) => total + (item.variant?.price || item.product.price) * item.quantity, + 0 + ); + }, + getItemCount: () => { + return get().items.reduce((count, item) => count + item.quantity, 0); + }, + }), + { + name: 'ecommerce-cart', + } + ) +); +""" + +AUTH_STORE = r"""import { create } from 'zustand'; +import { persist } from 'zustand/middleware'; +import type { AuthUser } from '../lib/auth'; + +interface AuthStore { + user: AuthUser | null; + token: string | null; + isLoading: boolean; + setAuth: (user: AuthUser, token: string) => void; + logout: () => void; + setLoading: (loading: boolean) => void; +} + +export const useAuthStore = create()( + persist( + (set) => ({ + user: null, + token: null, + isLoading: false, + setAuth: (user, token) => set({ user, token }), + logout: () => set({ user: null, token: null }), + setLoading: (isLoading) => set({ isLoading }), + }), + { + name: 'ecommerce-auth', + } + ) +); +""" + +VENDOR_STORE = r"""import { create } from 'zustand'; +import { supabase } from '../lib/supabase'; +import type { Product, Order, VendorProfile } from '../lib/types'; + +interface VendorStore { + profile: VendorProfile | null; + products: Product[]; + orders: Order[]; + isLoading: boolean; + fetchProfile: (userId: string) => Promise; + fetchProducts: (vendorId: string) => Promise; + fetchOrders: (vendorId: string) => Promise; + createProduct: (product: Partial) => Promise; + updateProduct: (id: string, updates: Partial) => Promise; + updateOrderStatus: (orderId: string, status: string) => Promise; +} + +export const useVendorStore = create((set, get) => ({ + profile: null, + products: [], + orders: [], + isLoading: false, + fetchProfile: async (userId) => { + set({ isLoading: true }); + const { data } = await supabase + .from('vendor_profiles') + .select('*') + .eq('user_id', userId) + .single(); + set({ profile: data, isLoading: false }); + }, + fetchProducts: async (vendorId) => { + set({ isLoading: true }); + const { data } = await supabase + .from('products') + .select('*') + .eq('vendor_id', vendorId) + .order('created_at', { ascending: false }); + set({ products: data || [], isLoading: false }); + }, + fetchOrders: async (vendorId) => { + set({ isLoading: true }); + const { data } = await supabase + .from('orders') + .select('*') + .eq('vendor_id', vendorId) + .order('created_at', { ascending: false }); + set({ orders: data || [], isLoading: false }); + }, + createProduct: async (product) => { + const { data, error } = await supabase + .from('products') + .insert(product) + .select() + .single(); + if (error) return null; + set((state) => ({ products: [data, ...state.products] })); + return data; + }, + updateProduct: async (id, updates) => { + await supabase.from('products').update(updates).eq('id', id); + set((state) => ({ + products: state.products.map((p) => + p.id === id ? { ...p, ...updates } : p + ), + })); + }, + updateOrderStatus: async (orderId, status) => { + await supabase.from('orders').update({ status }).eq('id', orderId); + set((state) => ({ + orders: state.orders.map((o) => + o.id === orderId ? { ...o, status: status as Order['status'] } : o + ), + })); + }, +})); +""" + +BASE_LAYOUT = """--- +interface Props {{ + title: string; + description?: string; +}} + +const {{ title, description = '{site_name}' }} = Astro.props; +--- + + + + + + + + + {{title}} | {site_name} + + + + + + + +""" + +TH_JSON = """{{ + "common": {{ + "home": "หน้าแรก", + "products": "สินค้า", + "cart": "ตะกร้า", + "checkout": "ชำระเงิน", + "login": "เข้าสู่ระบบ", + "register": "ลงทะเบียน", + "logout": "ออกจากระบบ" + }}, + "product": {{ + "addToCart": "เพิ่มลงตะกร้า", + "outOfStock": "สินค้าหมด", + "inStock": "มีสินค้า" + }}, + "cart": {{ + "empty": "ตะกร้าว่าง", + "total": "รวม" + }} +}}""" + +EN_JSON = """{{ + "common": {{ + "home": "Home", + "products": "Products", + "cart": "Cart", + "checkout": "Checkout", + "login": "Login", + "register": "Register", + "logout": "Logout" + }}, + "product": {{ + "addToCart": "Add to Cart", + "outOfStock": "Out of Stock", + "inStock": "In Stock" + }}, + "cart": {{ + "empty": "Your cart is empty", + "total": "Total" + }} +}}""" + +GLOBAL_CSS = """@import "tailwindcss"; + +@theme { + --font-sans: "Inter", system-ui, sans-serif; + --color-primary: #2563eb; + --color-secondary: #64748b; +}""" + +CART_BUTTON = r"""import { useCartStore } from '../../stores/cart'; + +export function CartButton() { + const getItemCount = useCartStore((state) => state.getItemCount); + const count = getItemCount(); + + return ( + + + + + {count > 0 && ( + + {count > 9 ? '9+' : count} + + )} + + ); +}""" + +CART_DRAWER = r"""import { useState } from 'react'; +import { useCartStore } from '../../stores/cart'; +import { formatPrice } from '../../lib/utils'; +import { CartItem } from './CartItem'; + +export function CartDrawer() { + const [isOpen, setIsOpen] = useState(false); + const items = useCartStore((state) => state.items); + const getTotal = useCartStore((state) => state.getTotal); + + return ( + <> + + + {isOpen && ( +
+
setIsOpen(false)} /> +
+
+
+

ตะกร้าสินค้า

+ +
+ +
+ {items.length === 0 ? ( +

ตะกร้าว่างเปล่า

+ ) : ( +
+ {items.map((item) => ( + + ))} +
+ )} +
+ + {items.length > 0 && ( +
+
+ รวม + {formatPrice(getTotal())} +
+ + ชำระเงิน + +
+ )} +
+
+
+ )} + + ); +}""" + +CART_ITEM_COMPONENT = r"""import { useCartStore } from '../../stores/cart'; +import { formatPrice } from '../../lib/utils'; +import type { CartItem as CartItemType } from '../../lib/types'; + +interface Props { + item: CartItemType; +} + +export function CartItem({ item }: Props) { + const updateQuantity = useCartStore((state) => state.updateQuantity); + const removeItem = useCartStore((state) => state.removeItem); + const price = item.variant?.price || item.product.price; + + return ( +
+ {item.product.name} +
+

{item.product.name}

+ {item.variant && ( +

{item.variant.name}

+ )} +

+ {formatPrice(price)} +

+
+
+ +
+ + {item.quantity} + +
+
+
+ ); +}""" + +PRODUCT_CARD = r"""import { Link } from '@astrojs/react/components'; +import { formatPrice } from '../../lib/utils'; +import type { Product } from '../../lib/types'; + +interface Props { + product: Product; +} + +export function ProductCard({ product }: Props) { + const isOutOfStock = product.inventory <= 0 && !product.allow_backorder; + const isLowStock = product.inventory > 0 && product.inventory <= product.low_stock_threshold; + + return ( + +
+
+ {product.name} + {isOutOfStock && ( +
+ + สินค้าหมด + +
+ )} + {product.featured && !isOutOfStock && ( + + แนะนำ + + )} +
+
+

+ {product.name} +

+
+ + {formatPrice(product.price)} + + {product.compare_at_price && ( + + {formatPrice(product.compare_at_price)} + + )} +
+ {isLowStock && ( +

สินค้าใกล้หมด ({product.inventory} ชิ้น)

+ )} +
+
+ + ); +}""" + +CHECKOUT_FORM = r"""import { useState } from 'react'; +import { useCartStore } from '../../stores/cart'; +import { useAuthStore } from '../../stores/auth'; +import { formatPrice, generateOrderNumber } from '../../lib/utils'; + +export function CheckoutForm() { + const items = useCartStore((state) => state.items); + const getTotal = useCartStore((state) => state.getTotal); + const clearCart = useCartStore((state) => state.clearCart); + const user = useAuthStore((state) => state.user); + + const [formData, setFormData] = useState({ + name: user?.name || '', + phone: '', + address: '', + city: '', + postal: '', + paymentMethod: 'qr', + }); + const [isSubmitting, setIsSubmitting] = useState(false); + + const handleSubmit = async (e: React.FormEvent) => { + e.preventDefault(); + setIsSubmitting(true); + + try { + const response = await fetch('/api/checkout/create-order', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ + ...formData, + items: items.map((item) => ({ + product_id: item.product.id, + variant_id: item.variant?.id, + quantity: item.quantity, + unit_price: item.variant?.price || item.product.price, + })), + order_number: generateOrderNumber(), + }), + }); + + const data = await response.json(); + + if (data.payment_url) { + window.location.href = data.payment_url; + } else { + clearCart(); + window.location.href = `/orders/${data.order_id}`; + } + } catch (error) { + console.error('Checkout error:', error); + } finally { + setIsSubmitting(false); + } + }; + + return ( +
+
+

ข้อมูลจัดส่ง

+
+
+ + setFormData({ ...formData, name: e.target.value })} + className="w-full px-4 py-2 border rounded-lg focus:ring-2 focus:ring-primary focus:border-primary" + /> +
+
+ + setFormData({ ...formData, phone: e.target.value })} + className="w-full px-4 py-2 border rounded-lg focus:ring-2 focus:ring-primary focus:border-primary" + /> +
+
+ + +``` + +### Custom Plugin + +```javascript +// tailwind.config.js +const plugin = require('tailwindcss/plugin') + +export default { + plugins: [ + plugin(function({ addUtilities, addComponents, theme }) { + // Add utilities + addUtilities({ + '.text-shadow': { + textShadow: '2px 2px 4px rgba(0, 0, 0, 0.1)', + }, + '.text-shadow-lg': { + textShadow: '4px 4px 8px rgba(0, 0, 0, 0.2)', + }, + }) + + // Add components + addComponents({ + '.card-custom': { + backgroundColor: theme('colors.white'), + borderRadius: theme('borderRadius.lg'), + padding: theme('spacing.6'), + boxShadow: theme('boxShadow.md'), + }, + }) + }), + ], +} +``` + +## Configuration Examples + +### Complete Tailwind Config + +```javascript +// tailwind.config.ts +import type { Config } from 'tailwindcss' + +const config: Config = { + darkMode: ["class"], + content: [ + './pages/**/*.{ts,tsx}', + './components/**/*.{ts,tsx}', + './app/**/*.{ts,tsx}', + ], + theme: { + container: { + center: true, + padding: "2rem", + screens: { + "2xl": "1400px", + }, + }, + extend: { + colors: { + border: "hsl(var(--border))", + input: "hsl(var(--input))", + ring: "hsl(var(--ring))", + background: "hsl(var(--background))", + foreground: "hsl(var(--foreground))", + primary: { + DEFAULT: "hsl(var(--primary))", + foreground: "hsl(var(--primary-foreground))", + }, + brand: { + 50: '#f0f9ff', + 500: '#3b82f6', + 900: '#1e3a8a', + }, + }, + fontFamily: { + sans: ['Inter', 'system-ui', 'sans-serif'], + display: ['Playfair Display', 'serif'], + }, + spacing: { + '18': '4.5rem', + '88': '22rem', + '128': '32rem', + }, + borderRadius: { + lg: "var(--radius)", + md: "calc(var(--radius) - 2px)", + sm: "calc(var(--radius) - 4px)", + }, + keyframes: { + "slide-in": { + "0%": { transform: "translateX(-100%)" }, + "100%": { transform: "translateX(0)" }, + }, + }, + animation: { + "slide-in": "slide-in 0.5s ease-out", + }, + }, + }, + plugins: [require("tailwindcss-animate")], +} + +export default config +``` + +## Dark Mode Configuration + +```javascript +// tailwind.config.js +export default { + darkMode: ["class"], // or "media" for automatic + // ... +} +``` + +**Usage:** +```html + + +
+ Responds to .dark class +
+ + + +
+ Responds to system preference automatically +
+``` + +## Content Configuration + +Specify files to scan for classes: + +```javascript +// tailwind.config.js +export default { + content: [ + "./src/**/*.{js,jsx,ts,tsx}", + "./app/**/*.{js,jsx,ts,tsx}", + "./components/**/*.{js,jsx,ts,tsx}", + "./pages/**/*.{js,jsx,ts,tsx}", + ], + // ... +} +``` + +### Safelist + +Preserve dynamic classes: + +```javascript +export default { + safelist: [ + 'bg-red-500', + 'bg-green-500', + 'bg-blue-500', + { + pattern: /bg-(red|green|blue)-(100|500|900)/, + }, + ], +} +``` + +## Best Practices + +1. **Use @theme for simple customizations**: Prefer CSS-based customization +2. **Extract components sparingly**: Use @apply only for truly repeated patterns +3. **Leverage design tokens**: Define custom tokens in @theme +4. **Layer organization**: Keep base, components, and utilities separate +5. **Plugin for complex logic**: Use plugins for advanced customizations +6. **Test dark mode**: Ensure custom colors work in both themes +7. **Document custom utilities**: Add comments explaining custom classes +8. **Semantic naming**: Use descriptive names (primary not blue) diff --git a/skills/website-creator/ui-styling/references/tailwind-responsive.md b/skills/website-creator/ui-styling/references/tailwind-responsive.md new file mode 100644 index 0000000..f252e18 --- /dev/null +++ b/skills/website-creator/ui-styling/references/tailwind-responsive.md @@ -0,0 +1,382 @@ +# Tailwind CSS Responsive Design + +Mobile-first breakpoints, responsive utilities, and adaptive layouts. + +## Mobile-First Approach + +Tailwind uses mobile-first responsive design. Base styles apply to all screen sizes, then use breakpoint prefixes to override at larger sizes. + +```html + +
+
Item 1
+
Item 2
+
Item 3
+
Item 4
+
+``` + +## Breakpoint System + +**Default breakpoints:** + +| Prefix | Min Width | CSS Media Query | +|--------|-----------|-----------------| +| `sm:` | 640px | `@media (min-width: 640px)` | +| `md:` | 768px | `@media (min-width: 768px)` | +| `lg:` | 1024px | `@media (min-width: 1024px)` | +| `xl:` | 1280px | `@media (min-width: 1280px)` | +| `2xl:` | 1536px | `@media (min-width: 1536px)` | + +## Responsive Patterns + +### Layout Changes + +```html + +
+
Left
+
Right
+
+ + +
+
Item 1
+
Item 2
+
Item 3
+
+``` + +### Visibility + +```html + + + + +
+ Mobile only content +
+ + +
Mobile menu
+ +``` + +### Typography + +```html + +

+ Heading scales with screen size +

+ +

+ Body text scales appropriately +

+``` + +### Spacing + +```html + +
+ More padding on larger screens +
+ + +
+
Item 1
+
Item 2
+
+``` + +### Width + +```html + +
+ Responsive width +
+ + +
+ Centered with responsive max width +
+``` + +## Common Responsive Layouts + +### Sidebar Layout + +```html +
+ + + + +
+ Main content +
+
+``` + +### Card Grid + +```html +
+
Card 1
+
Card 2
+
Card 3
+
Card 4
+
+``` + +### Hero Section + +```html +
+
+
+
+

+ Hero Title +

+

+ Hero description +

+ +
+
+ +
+
+
+
+``` + +### Navigation + +```html + +``` + +## Max-Width Queries + +Apply styles only below certain breakpoint using `max-*:` prefix: + +```html + +
+ Centered on mobile/tablet, left-aligned on desktop +
+ + +
+ Hidden only on mobile +
+``` + +Available: `max-sm:` `max-md:` `max-lg:` `max-xl:` `max-2xl:` + +## Range Queries + +Apply styles between breakpoints: + +```html + +
+ Visible only on tablets +
+ + +
+ 2 columns on tablet, 4 on extra large +
+``` + +## Container Queries + +Style elements based on parent container width: + +```html +
+
+ Responds to parent width, not viewport +
+
+``` + +Container query breakpoints: `@sm:` `@md:` `@lg:` `@xl:` `@2xl:` + +## Custom Breakpoints + +Define custom breakpoints in theme: + +```css +@theme { + --breakpoint-3xl: 120rem; /* 1920px */ + --breakpoint-tablet: 48rem; /* 768px */ +} +``` + +```html +
+ Uses custom breakpoints +
+``` + +## Responsive State Variants + +Combine responsive with hover/focus: + +```html + + + + + + Link + +``` + +## Best Practices + +### 1. Mobile-First Design + +Start with mobile styles, add complexity at larger breakpoints: + +```html + +
+ + +
+``` + +### 2. Consistent Breakpoint Usage + +Use same breakpoints across related elements: + +```html +
+ Spacing scales with layout +
+``` + +### 3. Test at Breakpoint Boundaries + +Test at exact breakpoint widths (640px, 768px, 1024px, etc.) to catch edge cases. + +### 4. Use Container for Content Width + +```html +
+
+ Content with consistent max width +
+
+``` + +### 5. Progressive Enhancement + +Ensure core functionality works on mobile, enhance for larger screens: + +```html + +
+ +
+ Content +
+
+``` + +### 6. Avoid Too Many Breakpoints + +Use 2-3 breakpoints per element for maintainability: + +```html + +
+ + +
+``` + +## Common Responsive Utilities + +### Responsive Display + +```html +
+ Changes display type per breakpoint +
+``` + +### Responsive Position + +```html +
+ Positioned differently per breakpoint +
+``` + +### Responsive Order + +```html +
+
First on desktop
+
First on mobile
+
+``` + +### Responsive Overflow + +```html +
+ Scrollable on mobile, expanded on desktop +
+``` + +## Testing Checklist + +- [ ] Test at 320px (small mobile) +- [ ] Test at 640px (mobile breakpoint) +- [ ] Test at 768px (tablet breakpoint) +- [ ] Test at 1024px (desktop breakpoint) +- [ ] Test at 1280px (large desktop breakpoint) +- [ ] Test landscape orientation +- [ ] Verify touch targets (min 44x44px) +- [ ] Check text readability at all sizes +- [ ] Verify navigation works on mobile +- [ ] Test with browser zoom diff --git a/skills/website-creator/ui-styling/references/tailwind-utilities.md b/skills/website-creator/ui-styling/references/tailwind-utilities.md new file mode 100644 index 0000000..7b7b123 --- /dev/null +++ b/skills/website-creator/ui-styling/references/tailwind-utilities.md @@ -0,0 +1,455 @@ +# Tailwind CSS Utility Reference + +Core utility classes for layout, spacing, typography, colors, borders, and shadows. + +## Layout Utilities + +### Display + +```html +
Block
+
Inline Block
+
Inline
+
Flexbox
+
Inline Flex
+
Grid
+
Inline Grid
+ +``` + +### Flexbox + +**Container:** +```html +
Row (default)
+
Column
+
Reverse row
+
Reverse column
+``` + +**Justify (main axis):** +```html +
Start
+
Center
+
End
+
Space between
+
Space around
+
Space evenly
+``` + +**Align (cross axis):** +```html +
Start
+
Center
+
End
+
Baseline
+
Stretch
+``` + +**Gap:** +```html +
All sides
+
X and Y
+``` + +**Wrap:** +```html +
Wrap
+
No wrap
+``` + +### Grid + +**Columns:** +```html +
1 column
+
2 columns
+
3 columns
+
4 columns
+
12 columns
+
Custom
+``` + +**Rows:** +```html +
3 rows
+
Custom
+``` + +**Span:** +```html +
Span 2 columns
+
Span 3 rows
+``` + +**Gap:** +```html +
All sides
+
X and Y
+``` + +### Positioning + +```html +
Static (default)
+
Relative
+
Absolute
+
Fixed
+
Sticky
+ + +
Top right
+
All sides 0
+
Left/right 4
+
Top/bottom 8
+``` + +### Z-Index + +```html +
z-index: 0
+
z-index: 10
+
z-index: 20
+
z-index: 50
+``` + +## Spacing Utilities + +### Padding + +```html +
All sides
+
Left and right
+
Top and bottom
+
Top
+
Right
+
Bottom
+
Left
+``` + +### Margin + +```html +
All sides
+
Center horizontally
+
Top and bottom
+
Top
+
Negative top
+
Push to right
+``` + +### Space Between + +```html +
Horizontal spacing
+
Vertical spacing
+``` + +### Spacing Scale + +- `0`: 0px +- `px`: 1px +- `0.5`: 0.125rem (2px) +- `1`: 0.25rem (4px) +- `2`: 0.5rem (8px) +- `3`: 0.75rem (12px) +- `4`: 1rem (16px) +- `6`: 1.5rem (24px) +- `8`: 2rem (32px) +- `12`: 3rem (48px) +- `16`: 4rem (64px) +- `24`: 6rem (96px) + +## Typography + +### Font Size + +```html +

Extra small (12px)

+

Small (14px)

+

Base (16px)

+

Large (18px)

+

XL (20px)

+

2XL (24px)

+

3XL (30px)

+

4XL (36px)

+

5XL (48px)

+``` + +### Font Weight + +```html +

Thin (100)

+

Light (300)

+

Normal (400)

+

Medium (500)

+

Semibold (600)

+

Bold (700)

+

Black (900)

+``` + +### Text Alignment + +```html +

Left

+

Center

+

Right

+

Justify

+``` + +### Line Height + +```html +

1

+

1.25

+

1.5

+

1.75

+

2

+``` + +### Combined Font Utilities + +```html +

+ Font size 4xl with tight line height +

+``` + +### Text Transform + +```html +

UPPERCASE

+

lowercase

+

Capitalize

+

Normal

+``` + +### Text Decoration + +```html +

Underline

+

Line through

+

No underline

+``` + +### Text Overflow + +```html +

Truncate with ellipsis...

+

Clamp to 3 lines...

+

Ellipsis

+``` + +## Colors + +### Text Colors + +```html +

Black

+

White

+

Gray 500

+

Red 600

+

Blue 500

+

Green 600

+``` + +### Background Colors + +```html +
White
+
Gray 100
+
Blue 500
+
Red 600
+``` + +### Color Scale + +Each color has 11 shades (50-950): +- `50`: Lightest +- `100-400`: Light variations +- `500`: Base color +- `600-800`: Dark variations +- `950`: Darkest + +### Opacity Modifiers + +```html +
75% opacity
+
30% opacity
+
87% opacity
+``` + +### Gradients + +```html +
+ Left to right gradient +
+
+ With via color +
+``` + +Directions: `to-t | to-tr | to-r | to-br | to-b | to-bl | to-l | to-tl` + +## Borders + +### Border Width + +```html +
1px all sides
+
2px all sides
+
Top only
+
Right 4px
+
Bottom 2px
+
Left only
+
No border
+``` + +### Border Color + +```html +
Gray
+
Blue
+
Red with opacity
+``` + +### Border Radius + +```html +
0.25rem
+
0.375rem
+
0.5rem
+
0.75rem
+
1rem
+
9999px
+ + +
Top corners
+
Bottom right
+``` + +### Border Style + +```html +
Solid
+
Dashed
+
Dotted
+``` + +## Shadows + +```html +
Small
+
Default
+
Medium
+
Large
+
Extra large
+
2XL
+
No shadow
+``` + +### Colored Shadows + +```html +
Blue shadow
+``` + +## Width & Height + +### Width + +```html +
100%
+
50%
+
33.333%
+
16rem
+
500px
+
100vw
+ + +
min-width: 0
+
max-width: 28rem
+
max-width: 1280px
+``` + +### Height + +```html +
100%
+
100vh
+
16rem
+
500px
+ + +
min-height: 100vh
+
max-height: 24rem
+``` + +## Arbitrary Values + +Use square brackets for custom values: + +```html + +
Custom padding
+
Custom position
+ + +
Hex color
+
RGB
+ + +
Custom width
+
Custom font size
+ + +
CSS var
+ + +
Custom grid
+``` + +## Aspect Ratio + +```html +
1:1
+
16:9
+
4:3
+``` + +## Overflow + +```html +
Auto scroll
+
Hidden
+
Always scroll
+
Horizontal scroll
+
No vertical scroll
+``` + +## Opacity + +```html +
0%
+
50%
+
75%
+
100%
+``` + +## Cursor + +```html +
Pointer
+
Wait
+
Not allowed
+
Default
+``` + +## User Select + +```html +
No select
+
Text selectable
+
Select all
+``` diff --git a/skills/website-creator/ui-styling/scripts/.coverage b/skills/website-creator/ui-styling/scripts/.coverage new file mode 100644 index 0000000000000000000000000000000000000000..43821425940ddaf25a0ae1505e659d19ab5e4f6d GIT binary patch literal 53248 zcmeI)OKcQ%90%~3ooTn**I+7cYD4<>AZ?-T(s(dZMXGWjKma8WLD|mE{&zcccV?NH zrL7Tk!Gwgwn~9kCh-ZTsO^eoxXNe&uQuOAD5bNV$I4CCi`_E$^&=Md`Nbx)AZs+lT ze*UkW-F9EvyVLYI%h^s*_t+EC8cCL=t&B;Ml%PikJ)$j6J8E=5zva69<#rR&@R^4@ zwEa?h^V^bkxMR0A)c$_Q`nK=ei-|LBAI9~BK^L$=00Izz!2esIvbQ~+?CzFN9{2Q2 zfqRZ_a3^@~c>cw0WBazTePhq<+{S`)tiP4fHZ;V>m}5_|5_ee6EO2I8S<}!x)5LUm0CRYbJKQq38T5%3 zjf)D62QkCE>U3(6mxvh#4)Byt@3?K2Xv>_WQlqu zpLUNH==D}wFMGCsoTkQ4kI+;5TMxCwlfAw2>7#x}5j^RP>ABUNL_?>ko?fQwi0jMu z8!<{6JBb)Yg#xp4{*V!|SPD2BebS*WM-yOJ!i0f}LsXYkrS$ zISmk+3!-4TvSo;&;)*&L_A&10c|IIo4W@u%YJCX(Atl2MG>EE$cF@CsFDDBheL>shWk zVYu{2xYJUJtMTO8wQ?mG5n^5vkEVu7h%>bzaMMC?=fyAS-UPd~?BX;PysxP_Il6Ya zf{P-gN5pgUsY5Zp$fvt~{US~J;-NGYS)8dAc?X{vI;IzBHYw?*LqnG4;9vz{B4Adh zz!}eJo?! z1+H7c+CVY3yOgKtGQSMBu1e3=(r{-)8u?>=ewm`hMNISiXwXIDi*|}d-O7gRl~{&@ zRB7%PgHz-aOd=GK_0gboX^7sGW|U}nRm@mX`okqM5vdmSY%ttKqtvB1HNkI5E?A9T z&^_*%Mb1PJzc7Bsn$lfnIGirhS~}3D+^!pl)=H0vXK|O@B3{Z>dgXX><3_ns4hLf} z&!k;i)Qfsl?C>zw&`EB(SnDslX~LyfJN5lU*wt}}!ypGSxdknl2^6AG;cK1N0da#b zYd0kN!3F^cKmY;|fB*y_009U<00Izzz=J2C$T2xC?*C)j&yw~#Jz;|Y1Rwwb2tWV= z5P$##AOHafKwwo0w8xZnTJ#qlo7TumPj~n?08b8&j0|sTrCPz{} zfB*y_009U<00Izz00iz)AgiiUWml?bd8uq_YHBKF6!dbIPnuqBZ-`rY`pyyVq=tj5 zDRwV9CE!G`hJUi$~7FbY8dh4*W~nwgnG)Z-a*2z2gtckNM|}RTc5+F3x|3_~bzZ#3 zCPp*}KmY;|fB*y_009U<00IzzfFyCXvP)jUd)X!L|J9OOF;+5{+8y)K_y5W(YGrf< zGi`SRrC?BwFYxyuL5 zbBpswj=z5K{G|nXy`m-(dtQ5a?ypmqXBQ40n46!Qo11**gV_uDOBa4xSXi*~U(C%} z@hve*`Gl%&X_F2w?)YQ4f4u3(f4e@5?f<6r%dMZs4t&-A%cI5Fru}VaAFC@*b@$B; zGR5B?Q%^ins>Hs2w?*pt_2ch8T5KEdzv0T2S+yEcl3M)#|JSZc+Hcz5+Ewk(x@*{l z00bZa0SG_<0uX=z1Rwwb2tZ&}2{h9i0I~HLI#JC@cgZ z009U<00Izz00bZa0SG_<0(U7OKL5x4|6QUXI0PU70SG_<0uX=z1Rwwb2tZ(!3E=bp eRn}lAE(9O|0SG_<0uX=z1Rwwb2teQ-1pWg|46iW& literal 0 HcmV?d00001 diff --git a/skills/website-creator/ui-styling/scripts/requirements.txt b/skills/website-creator/ui-styling/scripts/requirements.txt new file mode 100644 index 0000000..75f72ca --- /dev/null +++ b/skills/website-creator/ui-styling/scripts/requirements.txt @@ -0,0 +1,17 @@ +# UI Styling Skill Dependencies +# Python 3.10+ required + +# No Python package dependencies - uses only standard library + +# Testing dependencies (dev) +pytest>=8.0.0 +pytest-cov>=4.1.0 +pytest-mock>=3.12.0 + +# Note: This skill works with shadcn/ui and Tailwind CSS +# Requires Node.js and package managers: +# - Node.js 18+: https://nodejs.org/ +# - npm (comes with Node.js) +# +# shadcn/ui CLI is installed per-project: +# npx shadcn-ui@latest init diff --git a/skills/website-creator/ui-styling/scripts/shadcn_add.py b/skills/website-creator/ui-styling/scripts/shadcn_add.py new file mode 100644 index 0000000..e2a9799 --- /dev/null +++ b/skills/website-creator/ui-styling/scripts/shadcn_add.py @@ -0,0 +1,292 @@ +#!/usr/bin/env python3 +""" +shadcn/ui Component Installer + +Add shadcn/ui components to project with automatic dependency handling. +Wraps shadcn CLI for programmatic component installation. +""" + +import argparse +import json +import subprocess +import sys +from pathlib import Path +from typing import List, Optional + + +class ShadcnInstaller: + """Handle shadcn/ui component installation.""" + + def __init__(self, project_root: Optional[Path] = None, dry_run: bool = False): + """ + Initialize installer. + + Args: + project_root: Project root directory (default: current directory) + dry_run: If True, show actions without executing + """ + self.project_root = project_root or Path.cwd() + self.dry_run = dry_run + self.components_json = self.project_root / "components.json" + + def check_shadcn_config(self) -> bool: + """ + Check if shadcn is initialized in project. + + Returns: + True if components.json exists + """ + return self.components_json.exists() + + def get_installed_components(self) -> List[str]: + """ + Get list of already installed components. + + Returns: + List of installed component names + """ + if not self.check_shadcn_config(): + return [] + + try: + with open(self.components_json) as f: + config = json.load(f) + + components_dir = self.project_root / config.get("aliases", {}).get( + "components", "components" + ).replace("@/", "") + ui_dir = components_dir / "ui" + + if not ui_dir.exists(): + return [] + + return [f.stem for f in ui_dir.glob("*.tsx") if f.is_file()] + except (json.JSONDecodeError, KeyError, OSError): + return [] + + def add_components( + self, components: List[str], overwrite: bool = False + ) -> tuple[bool, str]: + """ + Add shadcn/ui components. + + Args: + components: List of component names to add + overwrite: If True, overwrite existing components + + Returns: + Tuple of (success, message) + """ + if not components: + return False, "No components specified" + + if not self.check_shadcn_config(): + return ( + False, + "shadcn not initialized. Run 'npx shadcn@latest init' first", + ) + + # Check which components already exist + installed = self.get_installed_components() + already_installed = [c for c in components if c in installed] + + if already_installed and not overwrite: + return ( + False, + f"Components already installed: {', '.join(already_installed)}. " + "Use --overwrite to reinstall", + ) + + # Build command + cmd = ["npx", "shadcn@latest", "add"] + components + + if overwrite: + cmd.append("--overwrite") + + if self.dry_run: + return True, f"Would run: {' '.join(cmd)}" + + # Execute command + try: + result = subprocess.run( + cmd, + cwd=self.project_root, + capture_output=True, + text=True, + check=True, + ) + + success_msg = f"Successfully added components: {', '.join(components)}" + if result.stdout: + success_msg += f"\n\nOutput:\n{result.stdout}" + + return True, success_msg + + except subprocess.CalledProcessError as e: + error_msg = f"Failed to add components: {e.stderr or e.stdout or str(e)}" + return False, error_msg + except FileNotFoundError: + return False, "npx not found. Ensure Node.js is installed" + + def add_all_components(self, overwrite: bool = False) -> tuple[bool, str]: + """ + Add all available shadcn/ui components. + + Args: + overwrite: If True, overwrite existing components + + Returns: + Tuple of (success, message) + """ + if not self.check_shadcn_config(): + return ( + False, + "shadcn not initialized. Run 'npx shadcn@latest init' first", + ) + + cmd = ["npx", "shadcn@latest", "add", "--all"] + + if overwrite: + cmd.append("--overwrite") + + if self.dry_run: + return True, f"Would run: {' '.join(cmd)}" + + try: + result = subprocess.run( + cmd, + cwd=self.project_root, + capture_output=True, + text=True, + check=True, + ) + + success_msg = "Successfully added all components" + if result.stdout: + success_msg += f"\n\nOutput:\n{result.stdout}" + + return True, success_msg + + except subprocess.CalledProcessError as e: + error_msg = f"Failed to add all components: {e.stderr or e.stdout or str(e)}" + return False, error_msg + except FileNotFoundError: + return False, "npx not found. Ensure Node.js is installed" + + def list_installed(self) -> tuple[bool, str]: + """ + List installed components. + + Returns: + Tuple of (success, message with component list) + """ + if not self.check_shadcn_config(): + return False, "shadcn not initialized" + + installed = self.get_installed_components() + + if not installed: + return True, "No components installed" + + return True, f"Installed components:\n" + "\n".join(f" - {c}" for c in sorted(installed)) + + +def main(): + """CLI entry point.""" + parser = argparse.ArgumentParser( + description="Add shadcn/ui components to your project", + formatter_class=argparse.RawDescriptionHelpFormatter, + epilog=""" +Examples: + # Add single component + python shadcn_add.py button + + # Add multiple components + python shadcn_add.py button card dialog + + # Add all components + python shadcn_add.py --all + + # Overwrite existing components + python shadcn_add.py button --overwrite + + # Dry run (show what would be done) + python shadcn_add.py button card --dry-run + + # List installed components + python shadcn_add.py --list + """, + ) + + parser.add_argument( + "components", + nargs="*", + help="Component names to add (e.g., button, card, dialog)", + ) + + parser.add_argument( + "--all", + action="store_true", + help="Add all available components", + ) + + parser.add_argument( + "--overwrite", + action="store_true", + help="Overwrite existing components", + ) + + parser.add_argument( + "--dry-run", + action="store_true", + help="Show what would be done without executing", + ) + + parser.add_argument( + "--list", + action="store_true", + help="List installed components", + ) + + parser.add_argument( + "--project-root", + type=Path, + help="Project root directory (default: current directory)", + ) + + args = parser.parse_args() + + # Initialize installer + installer = ShadcnInstaller( + project_root=args.project_root, + dry_run=args.dry_run, + ) + + # Handle list command + if args.list: + success, message = installer.list_installed() + print(message) + sys.exit(0 if success else 1) + + # Handle add all command + if args.all: + success, message = installer.add_all_components(overwrite=args.overwrite) + print(message) + sys.exit(0 if success else 1) + + # Handle add specific components + if not args.components: + parser.print_help() + sys.exit(1) + + success, message = installer.add_components( + args.components, + overwrite=args.overwrite, + ) + + print(message) + sys.exit(0 if success else 1) + + +if __name__ == "__main__": + main() diff --git a/skills/website-creator/ui-styling/scripts/tailwind_config_gen.py b/skills/website-creator/ui-styling/scripts/tailwind_config_gen.py new file mode 100644 index 0000000..5109311 --- /dev/null +++ b/skills/website-creator/ui-styling/scripts/tailwind_config_gen.py @@ -0,0 +1,456 @@ +#!/usr/bin/env python3 +""" +Tailwind CSS Configuration Generator + +Generate tailwind.config.js/ts with custom theme configuration. +Supports colors, fonts, spacing, breakpoints, and plugin recommendations. +""" + +import argparse +import json +import sys +from pathlib import Path +from typing import Any, Dict, List, Optional + + +class TailwindConfigGenerator: + """Generate Tailwind CSS configuration files.""" + + def __init__( + self, + typescript: bool = True, + framework: str = "react", + output_path: Optional[Path] = None, + ): + """ + Initialize generator. + + Args: + typescript: If True, generate .ts config, else .js + framework: Framework name (react, vue, svelte, nextjs) + output_path: Output file path (default: auto-detect) + """ + self.typescript = typescript + self.framework = framework + self.output_path = output_path or self._default_output_path() + self.config: Dict[str, Any] = self._base_config() + + def _default_output_path(self) -> Path: + """Determine default output path.""" + ext = "ts" if self.typescript else "js" + return Path.cwd() / f"tailwind.config.{ext}" + + def _base_config(self) -> Dict[str, Any]: + """Create base configuration structure.""" + return { + "darkMode": ["class"], + "content": self._default_content_paths(), + "theme": { + "extend": {} + }, + "plugins": [] + } + + def _default_content_paths(self) -> List[str]: + """Get default content paths for framework.""" + paths = { + "react": [ + "./src/**/*.{js,jsx,ts,tsx}", + "./index.html", + ], + "vue": [ + "./src/**/*.{vue,js,ts,jsx,tsx}", + "./index.html", + ], + "svelte": [ + "./src/**/*.{svelte,js,ts}", + "./src/app.html", + ], + "nextjs": [ + "./app/**/*.{js,ts,jsx,tsx}", + "./pages/**/*.{js,ts,jsx,tsx}", + "./components/**/*.{js,ts,jsx,tsx}", + ], + } + return paths.get(self.framework, paths["react"]) + + def add_colors(self, colors: Dict[str, str]) -> None: + """ + Add custom colors to theme. + + Args: + colors: Dict of color_name: color_value + Value can be hex (#3b82f6) or variable (hsl(var(--primary))) + """ + if "colors" not in self.config["theme"]["extend"]: + self.config["theme"]["extend"]["colors"] = {} + + self.config["theme"]["extend"]["colors"].update(colors) + + def add_color_palette(self, name: str, base_color: str) -> None: + """ + Add full color palette (50-950 shades) for a base color. + + Args: + name: Color name (e.g., 'brand', 'primary') + base_color: Base color in oklch format or hex + """ + # For simplicity, use CSS variable approach + if "colors" not in self.config["theme"]["extend"]: + self.config["theme"]["extend"]["colors"] = {} + + self.config["theme"]["extend"]["colors"][name] = { + "50": f"var(--color-{name}-50)", + "100": f"var(--color-{name}-100)", + "200": f"var(--color-{name}-200)", + "300": f"var(--color-{name}-300)", + "400": f"var(--color-{name}-400)", + "500": f"var(--color-{name}-500)", + "600": f"var(--color-{name}-600)", + "700": f"var(--color-{name}-700)", + "800": f"var(--color-{name}-800)", + "900": f"var(--color-{name}-900)", + "950": f"var(--color-{name}-950)", + } + + def add_fonts(self, fonts: Dict[str, List[str]]) -> None: + """ + Add custom font families. + + Args: + fonts: Dict of font_type: [font_names] + e.g., {'sans': ['Inter', 'system-ui', 'sans-serif']} + """ + if "fontFamily" not in self.config["theme"]["extend"]: + self.config["theme"]["extend"]["fontFamily"] = {} + + self.config["theme"]["extend"]["fontFamily"].update(fonts) + + def add_spacing(self, spacing: Dict[str, str]) -> None: + """ + Add custom spacing values. + + Args: + spacing: Dict of name: value + e.g., {'18': '4.5rem', 'navbar': '4rem'} + """ + if "spacing" not in self.config["theme"]["extend"]: + self.config["theme"]["extend"]["spacing"] = {} + + self.config["theme"]["extend"]["spacing"].update(spacing) + + def add_breakpoints(self, breakpoints: Dict[str, str]) -> None: + """ + Add custom breakpoints. + + Args: + breakpoints: Dict of name: width + e.g., {'3xl': '1920px', 'tablet': '768px'} + """ + if "screens" not in self.config["theme"]["extend"]: + self.config["theme"]["extend"]["screens"] = {} + + self.config["theme"]["extend"]["screens"].update(breakpoints) + + def add_plugins(self, plugins: List[str]) -> None: + """ + Add plugin requirements. + + Args: + plugins: List of plugin names + e.g., ['@tailwindcss/typography', '@tailwindcss/forms'] + """ + for plugin in plugins: + if plugin not in self.config["plugins"]: + self.config["plugins"].append(plugin) + + def recommend_plugins(self) -> List[str]: + """ + Get plugin recommendations based on configuration. + + Returns: + List of recommended plugin package names + """ + recommendations = [] + + # Always recommend animation plugin + recommendations.append("tailwindcss-animate") + + # Framework-specific recommendations + if self.framework == "nextjs": + recommendations.append("@tailwindcss/typography") + + return recommendations + + def generate_config_string(self) -> str: + """ + Generate configuration file content. + + Returns: + Configuration file as string + """ + if self.typescript: + return self._generate_typescript() + return self._generate_javascript() + + def _generate_typescript(self) -> str: + """Generate TypeScript configuration.""" + plugins_str = self._format_plugins() + + config_json = json.dumps(self.config, indent=2) + + # Remove plugin array from JSON (we'll add it with require()) + config_obj = self.config.copy() + config_obj.pop("plugins", None) + config_json = json.dumps(config_obj, indent=2) + + return f"""import type {{ Config }} from 'tailwindcss' + +const config: Config = {{ +{self._indent_json(config_json, 1)} + plugins: [{plugins_str}], +}} + +export default config +""" + + def _generate_javascript(self) -> str: + """Generate JavaScript configuration.""" + plugins_str = self._format_plugins() + + config_obj = self.config.copy() + config_obj.pop("plugins", None) + config_json = json.dumps(config_obj, indent=2) + + return f"""/** @type {{import('tailwindcss').Config}} */ +module.exports = {{ +{self._indent_json(config_json, 1)} + plugins: [{plugins_str}], +}} +""" + + def _format_plugins(self) -> str: + """Format plugins array for config.""" + if not self.config["plugins"]: + return "" + + plugin_requires = [ + f"require('{plugin}')" for plugin in self.config["plugins"] + ] + return ", ".join(plugin_requires) + + def _indent_json(self, json_str: str, level: int) -> str: + """Add indentation to JSON string.""" + indent = " " * level + lines = json_str.split("\n") + # Skip first and last lines (braces) + indented = [indent + line for line in lines[1:-1]] + return "\n".join(indented) + + def write_config(self) -> tuple[bool, str]: + """ + Write configuration to file. + + Returns: + Tuple of (success, message) + """ + try: + config_content = self.generate_config_string() + + self.output_path.write_text(config_content) + + return True, f"Configuration written to {self.output_path}" + + except OSError as e: + return False, f"Failed to write config: {e}" + + def validate_config(self) -> tuple[bool, str]: + """ + Validate configuration. + + Returns: + Tuple of (valid, message) + """ + # Check content paths exist + if not self.config["content"]: + return False, "No content paths specified" + + # Check if extending empty theme + if not self.config["theme"]["extend"]: + return True, "Warning: No theme extensions defined" + + return True, "Configuration valid" + + +def main(): + """CLI entry point.""" + parser = argparse.ArgumentParser( + description="Generate Tailwind CSS configuration", + formatter_class=argparse.RawDescriptionHelpFormatter, + epilog=""" +Examples: + # Generate TypeScript config for Next.js + python tailwind_config_gen.py --framework nextjs + + # Generate JavaScript config with custom colors + python tailwind_config_gen.py --js --colors brand:#3b82f6 accent:#8b5cf6 + + # Add custom fonts + python tailwind_config_gen.py --fonts display:"Playfair Display,serif" + + # Add custom spacing and breakpoints + python tailwind_config_gen.py --spacing navbar:4rem --breakpoints 3xl:1920px + + # Add recommended plugins + python tailwind_config_gen.py --plugins + """, + ) + + parser.add_argument( + "--framework", + choices=["react", "vue", "svelte", "nextjs"], + default="react", + help="Target framework (default: react)", + ) + + parser.add_argument( + "--js", + action="store_true", + help="Generate JavaScript config instead of TypeScript", + ) + + parser.add_argument( + "--output", + type=Path, + help="Output file path", + ) + + parser.add_argument( + "--colors", + nargs="*", + metavar="NAME:VALUE", + help="Custom colors (e.g., brand:#3b82f6)", + ) + + parser.add_argument( + "--fonts", + nargs="*", + metavar="TYPE:FAMILY", + help="Custom fonts (e.g., sans:'Inter,system-ui')", + ) + + parser.add_argument( + "--spacing", + nargs="*", + metavar="NAME:VALUE", + help="Custom spacing (e.g., navbar:4rem)", + ) + + parser.add_argument( + "--breakpoints", + nargs="*", + metavar="NAME:WIDTH", + help="Custom breakpoints (e.g., 3xl:1920px)", + ) + + parser.add_argument( + "--plugins", + action="store_true", + help="Add recommended plugins", + ) + + parser.add_argument( + "--validate-only", + action="store_true", + help="Validate config without writing file", + ) + + args = parser.parse_args() + + # Initialize generator + generator = TailwindConfigGenerator( + typescript=not args.js, + framework=args.framework, + output_path=args.output, + ) + + # Add custom colors + if args.colors: + colors = {} + for color_spec in args.colors: + try: + name, value = color_spec.split(":", 1) + colors[name] = value + except ValueError: + print(f"Invalid color spec: {color_spec}", file=sys.stderr) + sys.exit(1) + generator.add_colors(colors) + + # Add custom fonts + if args.fonts: + fonts = {} + for font_spec in args.fonts: + try: + font_type, family = font_spec.split(":", 1) + fonts[font_type] = [f.strip().strip("'\"") for f in family.split(",")] + except ValueError: + print(f"Invalid font spec: {font_spec}", file=sys.stderr) + sys.exit(1) + generator.add_fonts(fonts) + + # Add custom spacing + if args.spacing: + spacing = {} + for spacing_spec in args.spacing: + try: + name, value = spacing_spec.split(":", 1) + spacing[name] = value + except ValueError: + print(f"Invalid spacing spec: {spacing_spec}", file=sys.stderr) + sys.exit(1) + generator.add_spacing(spacing) + + # Add custom breakpoints + if args.breakpoints: + breakpoints = {} + for bp_spec in args.breakpoints: + try: + name, width = bp_spec.split(":", 1) + breakpoints[name] = width + except ValueError: + print(f"Invalid breakpoint spec: {bp_spec}", file=sys.stderr) + sys.exit(1) + generator.add_breakpoints(breakpoints) + + # Add recommended plugins + if args.plugins: + recommended = generator.recommend_plugins() + generator.add_plugins(recommended) + print(f"Added recommended plugins: {', '.join(recommended)}") + print("\nInstall with:") + print(f" npm install -D {' '.join(recommended)}") + + # Validate + valid, message = generator.validate_config() + if not valid: + print(f"Validation failed: {message}", file=sys.stderr) + sys.exit(1) + + if message.startswith("Warning"): + print(message) + + # Validate only mode + if args.validate_only: + print("Configuration valid") + print("\nGenerated config:") + print(generator.generate_config_string()) + sys.exit(0) + + # Write config + success, message = generator.write_config() + print(message) + sys.exit(0 if success else 1) + + +if __name__ == "__main__": + main() diff --git a/skills/website-creator/ui-styling/scripts/tests/coverage-ui.json b/skills/website-creator/ui-styling/scripts/tests/coverage-ui.json new file mode 100644 index 0000000..2a20568 --- /dev/null +++ b/skills/website-creator/ui-styling/scripts/tests/coverage-ui.json @@ -0,0 +1 @@ +{"meta": {"format": 3, "version": "7.11.0", "timestamp": "2025-11-05T00:57:08.005243", "branch_coverage": false, "show_contexts": false}, "files": {"shadcn_add.py": {"executed_lines": [2, 9, 10, 11, 12, 13, 14, 17, 18, 20, 28, 29, 30, 32, 39, 41, 48, 49, 51, 52, 53, 55, 58, 60, 63, 67, 80, 81, 83, 84, 90, 91, 93, 94, 101, 103, 104, 106, 107, 110, 111, 119, 120, 121, 123, 125, 126, 127, 128, 129, 131, 141, 142, 147, 149, 152, 153, 155, 156, 164, 165, 166, 168, 176, 183, 184, 186, 188, 189, 191, 194, 291], "summary": {"covered_lines": 70, "num_statements": 103, "percent_covered": 67.96116504854369, "percent_covered_display": "68", "missing_lines": 33, "excluded_lines": 0}, "missing_lines": [61, 64, 65, 150, 170, 171, 172, 173, 174, 196, 221, 227, 233, 239, 245, 251, 257, 260, 266, 267, 268, 269, 272, 273, 274, 275, 278, 279, 280, 282, 287, 288, 292], "excluded_lines": [], "functions": {"ShadcnInstaller.__init__": {"executed_lines": [28, 29, 30], "summary": {"covered_lines": 3, "num_statements": 3, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 0}, "missing_lines": [], "excluded_lines": []}, "ShadcnInstaller.check_shadcn_config": {"executed_lines": [39], "summary": {"covered_lines": 1, "num_statements": 1, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 0}, "missing_lines": [], "excluded_lines": []}, "ShadcnInstaller.get_installed_components": {"executed_lines": [48, 49, 51, 52, 53, 55, 58, 60, 63], "summary": {"covered_lines": 9, "num_statements": 12, "percent_covered": 75.0, "percent_covered_display": "75", "missing_lines": 3, "excluded_lines": 0}, "missing_lines": [61, 64, 65], "excluded_lines": []}, "ShadcnInstaller.add_components": {"executed_lines": [80, 81, 83, 84, 90, 91, 93, 94, 101, 103, 104, 106, 107, 110, 111, 119, 120, 121, 123, 125, 126, 127, 128, 129], "summary": {"covered_lines": 24, "num_statements": 24, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 0}, "missing_lines": [], "excluded_lines": []}, "ShadcnInstaller.add_all_components": {"executed_lines": [141, 142, 147, 149, 152, 153, 155, 156, 164, 165, 166, 168], "summary": {"covered_lines": 12, "num_statements": 18, "percent_covered": 66.66666666666667, "percent_covered_display": "67", "missing_lines": 6, "excluded_lines": 0}, "missing_lines": [150, 170, 171, 172, 173, 174], "excluded_lines": []}, "ShadcnInstaller.list_installed": {"executed_lines": [183, 184, 186, 188, 189, 191], "summary": {"covered_lines": 6, "num_statements": 6, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 0}, "missing_lines": [], "excluded_lines": []}, "main": {"executed_lines": [], "summary": {"covered_lines": 0, "num_statements": 23, "percent_covered": 0.0, "percent_covered_display": "0", "missing_lines": 23, "excluded_lines": 0}, "missing_lines": [196, 221, 227, 233, 239, 245, 251, 257, 260, 266, 267, 268, 269, 272, 273, 274, 275, 278, 279, 280, 282, 287, 288], "excluded_lines": []}, "": {"executed_lines": [2, 9, 10, 11, 12, 13, 14, 17, 18, 20, 32, 41, 67, 131, 176, 194, 291], "summary": {"covered_lines": 15, "num_statements": 16, "percent_covered": 93.75, "percent_covered_display": "94", "missing_lines": 1, "excluded_lines": 0}, "missing_lines": [292], "excluded_lines": []}}, "classes": {"ShadcnInstaller": {"executed_lines": [28, 29, 30, 39, 48, 49, 51, 52, 53, 55, 58, 60, 63, 80, 81, 83, 84, 90, 91, 93, 94, 101, 103, 104, 106, 107, 110, 111, 119, 120, 121, 123, 125, 126, 127, 128, 129, 141, 142, 147, 149, 152, 153, 155, 156, 164, 165, 166, 168, 183, 184, 186, 188, 189, 191], "summary": {"covered_lines": 55, "num_statements": 64, "percent_covered": 85.9375, "percent_covered_display": "86", "missing_lines": 9, "excluded_lines": 0}, "missing_lines": [61, 64, 65, 150, 170, 171, 172, 173, 174], "excluded_lines": []}, "": {"executed_lines": [2, 9, 10, 11, 12, 13, 14, 17, 18, 20, 32, 41, 67, 131, 176, 194, 291], "summary": {"covered_lines": 15, "num_statements": 39, "percent_covered": 38.46153846153846, "percent_covered_display": "38", "missing_lines": 24, "excluded_lines": 0}, "missing_lines": [196, 221, 227, 233, 239, 245, 251, 257, 260, 266, 267, 268, 269, 272, 273, 274, 275, 278, 279, 280, 282, 287, 288, 292], "excluded_lines": []}}}, "tailwind_config_gen.py": {"executed_lines": [2, 9, 10, 11, 12, 13, 16, 17, 19, 33, 34, 35, 36, 38, 40, 41, 43, 45, 54, 56, 75, 77, 85, 86, 88, 90, 99, 100, 102, 116, 124, 125, 127, 129, 137, 138, 140, 142, 150, 151, 153, 155, 163, 164, 165, 167, 174, 177, 180, 181, 183, 185, 192, 193, 194, 196, 198, 200, 203, 204, 205, 207, 217, 219, 221, 222, 223, 225, 232, 234, 235, 237, 240, 242, 244, 245, 247, 248, 250, 257, 258, 260, 262, 264, 265, 267, 275, 276, 279, 280, 285, 455], "summary": {"covered_lines": 90, "num_statements": 164, "percent_covered": 54.8780487804878, "percent_covered_display": "55", "missing_lines": 74, "excluded_lines": 0}, "missing_lines": [282, 287, 309, 316, 322, 328, 335, 342, 349, 356, 362, 368, 371, 378, 379, 380, 381, 382, 383, 384, 385, 386, 387, 390, 391, 392, 393, 394, 395, 396, 397, 398, 399, 402, 403, 404, 405, 406, 407, 408, 409, 410, 411, 414, 415, 416, 417, 418, 419, 420, 421, 422, 423, 426, 427, 428, 429, 430, 431, 434, 435, 436, 437, 439, 440, 443, 444, 445, 446, 447, 450, 451, 452, 456], "excluded_lines": [], "functions": {"TailwindConfigGenerator.__init__": {"executed_lines": [33, 34, 35, 36], "summary": {"covered_lines": 4, "num_statements": 4, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 0}, "missing_lines": [], "excluded_lines": []}, "TailwindConfigGenerator._default_output_path": {"executed_lines": [40, 41], "summary": {"covered_lines": 2, "num_statements": 2, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 0}, "missing_lines": [], "excluded_lines": []}, "TailwindConfigGenerator._base_config": {"executed_lines": [45], "summary": {"covered_lines": 1, "num_statements": 1, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 0}, "missing_lines": [], "excluded_lines": []}, "TailwindConfigGenerator._default_content_paths": {"executed_lines": [56, 75], "summary": {"covered_lines": 2, "num_statements": 2, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 0}, "missing_lines": [], "excluded_lines": []}, "TailwindConfigGenerator.add_colors": {"executed_lines": [85, 86, 88], "summary": {"covered_lines": 3, "num_statements": 3, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 0}, "missing_lines": [], "excluded_lines": []}, "TailwindConfigGenerator.add_color_palette": {"executed_lines": [99, 100, 102], "summary": {"covered_lines": 3, "num_statements": 3, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 0}, "missing_lines": [], "excluded_lines": []}, "TailwindConfigGenerator.add_fonts": {"executed_lines": [124, 125, 127], "summary": {"covered_lines": 3, "num_statements": 3, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 0}, "missing_lines": [], "excluded_lines": []}, "TailwindConfigGenerator.add_spacing": {"executed_lines": [137, 138, 140], "summary": {"covered_lines": 3, "num_statements": 3, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 0}, "missing_lines": [], "excluded_lines": []}, "TailwindConfigGenerator.add_breakpoints": {"executed_lines": [150, 151, 153], "summary": {"covered_lines": 3, "num_statements": 3, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 0}, "missing_lines": [], "excluded_lines": []}, "TailwindConfigGenerator.add_plugins": {"executed_lines": [163, 164, 165], "summary": {"covered_lines": 3, "num_statements": 3, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 0}, "missing_lines": [], "excluded_lines": []}, "TailwindConfigGenerator.recommend_plugins": {"executed_lines": [174, 177, 180, 181, 183], "summary": {"covered_lines": 5, "num_statements": 5, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 0}, "missing_lines": [], "excluded_lines": []}, "TailwindConfigGenerator.generate_config_string": {"executed_lines": [192, 193, 194], "summary": {"covered_lines": 3, "num_statements": 3, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 0}, "missing_lines": [], "excluded_lines": []}, "TailwindConfigGenerator._generate_typescript": {"executed_lines": [198, 200, 203, 204, 205, 207], "summary": {"covered_lines": 6, "num_statements": 6, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 0}, "missing_lines": [], "excluded_lines": []}, "TailwindConfigGenerator._generate_javascript": {"executed_lines": [219, 221, 222, 223, 225], "summary": {"covered_lines": 5, "num_statements": 5, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 0}, "missing_lines": [], "excluded_lines": []}, "TailwindConfigGenerator._format_plugins": {"executed_lines": [234, 235, 237, 240], "summary": {"covered_lines": 4, "num_statements": 4, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 0}, "missing_lines": [], "excluded_lines": []}, "TailwindConfigGenerator._indent_json": {"executed_lines": [244, 245, 247, 248], "summary": {"covered_lines": 4, "num_statements": 4, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 0}, "missing_lines": [], "excluded_lines": []}, "TailwindConfigGenerator.write_config": {"executed_lines": [257, 258, 260, 262, 264, 265], "summary": {"covered_lines": 6, "num_statements": 6, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 0}, "missing_lines": [], "excluded_lines": []}, "TailwindConfigGenerator.validate_config": {"executed_lines": [275, 276, 279, 280], "summary": {"covered_lines": 4, "num_statements": 5, "percent_covered": 80.0, "percent_covered_display": "80", "missing_lines": 1, "excluded_lines": 0}, "missing_lines": [282], "excluded_lines": []}, "main": {"executed_lines": [], "summary": {"covered_lines": 0, "num_statements": 72, "percent_covered": 0.0, "percent_covered_display": "0", "missing_lines": 72, "excluded_lines": 0}, "missing_lines": [287, 309, 316, 322, 328, 335, 342, 349, 356, 362, 368, 371, 378, 379, 380, 381, 382, 383, 384, 385, 386, 387, 390, 391, 392, 393, 394, 395, 396, 397, 398, 399, 402, 403, 404, 405, 406, 407, 408, 409, 410, 411, 414, 415, 416, 417, 418, 419, 420, 421, 422, 423, 426, 427, 428, 429, 430, 431, 434, 435, 436, 437, 439, 440, 443, 444, 445, 446, 447, 450, 451, 452], "excluded_lines": []}, "": {"executed_lines": [2, 9, 10, 11, 12, 13, 16, 17, 19, 38, 43, 54, 77, 90, 116, 129, 142, 155, 167, 185, 196, 217, 232, 242, 250, 267, 285, 455], "summary": {"covered_lines": 26, "num_statements": 27, "percent_covered": 96.29629629629629, "percent_covered_display": "96", "missing_lines": 1, "excluded_lines": 0}, "missing_lines": [456], "excluded_lines": []}}, "classes": {"TailwindConfigGenerator": {"executed_lines": [33, 34, 35, 36, 40, 41, 45, 56, 75, 85, 86, 88, 99, 100, 102, 124, 125, 127, 137, 138, 140, 150, 151, 153, 163, 164, 165, 174, 177, 180, 181, 183, 192, 193, 194, 198, 200, 203, 204, 205, 207, 219, 221, 222, 223, 225, 234, 235, 237, 240, 244, 245, 247, 248, 257, 258, 260, 262, 264, 265, 275, 276, 279, 280], "summary": {"covered_lines": 64, "num_statements": 65, "percent_covered": 98.46153846153847, "percent_covered_display": "98", "missing_lines": 1, "excluded_lines": 0}, "missing_lines": [282], "excluded_lines": []}, "": {"executed_lines": [2, 9, 10, 11, 12, 13, 16, 17, 19, 38, 43, 54, 77, 90, 116, 129, 142, 155, 167, 185, 196, 217, 232, 242, 250, 267, 285, 455], "summary": {"covered_lines": 26, "num_statements": 99, "percent_covered": 26.262626262626263, "percent_covered_display": "26", "missing_lines": 73, "excluded_lines": 0}, "missing_lines": [287, 309, 316, 322, 328, 335, 342, 349, 356, 362, 368, 371, 378, 379, 380, 381, 382, 383, 384, 385, 386, 387, 390, 391, 392, 393, 394, 395, 396, 397, 398, 399, 402, 403, 404, 405, 406, 407, 408, 409, 410, 411, 414, 415, 416, 417, 418, 419, 420, 421, 422, 423, 426, 427, 428, 429, 430, 431, 434, 435, 436, 437, 439, 440, 443, 444, 445, 446, 447, 450, 451, 452, 456], "excluded_lines": []}}}, "tests/test_shadcn_add.py": {"executed_lines": [1, 3, 4, 5, 6, 8, 11, 12, 14, 17, 18, 20, 21, 23, 24, 27, 28, 39, 40, 42, 44, 46, 47, 48, 50, 52, 53, 55, 57, 58, 60, 62, 63, 65, 67, 68, 70, 72, 73, 74, 76, 78, 81, 82, 84, 85, 87, 89, 91, 92, 93, 95, 97, 98, 100, 101, 103, 105, 106, 108, 109, 111, 113, 114, 116, 117, 119, 120, 121, 123, 125, 126, 128, 130, 131, 136, 138, 139, 140, 143, 144, 146, 148, 149, 151, 152, 153, 154, 156, 157, 159, 165, 166, 168, 169, 170, 171, 174, 175, 176, 177, 178, 180, 181, 183, 187, 188, 190, 191, 193, 194, 196, 198, 199, 201, 202, 204, 206, 207, 209, 210, 212, 214, 215, 217, 218, 219, 221, 222, 224, 229, 230, 232, 233, 236, 237, 239, 241, 242, 244, 245, 247, 249, 250, 252, 253, 255, 257, 258, 259, 261, 262, 264, 265, 266], "summary": {"covered_lines": 153, "num_statements": 153, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 0}, "missing_lines": [], "excluded_lines": [], "functions": {"TestShadcnInstaller.temp_project": {"executed_lines": [23, 24, 27, 28, 39, 40, 42], "summary": {"covered_lines": 7, "num_statements": 7, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 0}, "missing_lines": [], "excluded_lines": []}, "TestShadcnInstaller.test_init_default_project_root": {"executed_lines": [46, 47, 48], "summary": {"covered_lines": 3, "num_statements": 3, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 0}, "missing_lines": [], "excluded_lines": []}, "TestShadcnInstaller.test_init_custom_project_root": {"executed_lines": [52, 53], "summary": {"covered_lines": 2, "num_statements": 2, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 0}, "missing_lines": [], "excluded_lines": []}, "TestShadcnInstaller.test_init_dry_run": {"executed_lines": [57, 58], "summary": {"covered_lines": 2, "num_statements": 2, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 0}, "missing_lines": [], "excluded_lines": []}, "TestShadcnInstaller.test_check_shadcn_config_exists": {"executed_lines": [62, 63], "summary": {"covered_lines": 2, "num_statements": 2, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 0}, "missing_lines": [], "excluded_lines": []}, "TestShadcnInstaller.test_check_shadcn_config_not_exists": {"executed_lines": [67, 68], "summary": {"covered_lines": 2, "num_statements": 2, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 0}, "missing_lines": [], "excluded_lines": []}, "TestShadcnInstaller.test_get_installed_components_empty": {"executed_lines": [72, 73, 74], "summary": {"covered_lines": 3, "num_statements": 3, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 0}, "missing_lines": [], "excluded_lines": []}, "TestShadcnInstaller.test_get_installed_components_with_files": {"executed_lines": [78, 81, 82, 84, 85, 87], "summary": {"covered_lines": 6, "num_statements": 6, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 0}, "missing_lines": [], "excluded_lines": []}, "TestShadcnInstaller.test_get_installed_components_no_config": {"executed_lines": [91, 92, 93], "summary": {"covered_lines": 3, "num_statements": 3, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 0}, "missing_lines": [], "excluded_lines": []}, "TestShadcnInstaller.test_add_components_no_components": {"executed_lines": [97, 98, 100, 101], "summary": {"covered_lines": 4, "num_statements": 4, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 0}, "missing_lines": [], "excluded_lines": []}, "TestShadcnInstaller.test_add_components_no_config": {"executed_lines": [105, 106, 108, 109], "summary": {"covered_lines": 4, "num_statements": 4, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 0}, "missing_lines": [], "excluded_lines": []}, "TestShadcnInstaller.test_add_components_already_installed": {"executed_lines": [113, 114, 116, 117, 119, 120, 121], "summary": {"covered_lines": 7, "num_statements": 7, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 0}, "missing_lines": [], "excluded_lines": []}, "TestShadcnInstaller.test_add_components_with_overwrite": {"executed_lines": [125, 126, 128, 130, 131, 136, 138, 139, 140, 143, 144], "summary": {"covered_lines": 11, "num_statements": 11, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 0}, "missing_lines": [], "excluded_lines": []}, "TestShadcnInstaller.test_add_components_dry_run": {"executed_lines": [148, 149, 151, 152, 153, 154], "summary": {"covered_lines": 6, "num_statements": 6, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 0}, "missing_lines": [], "excluded_lines": []}, "TestShadcnInstaller.test_add_components_success": {"executed_lines": [159, 165, 166, 168, 169, 170, 171, 174, 175, 176, 177, 178], "summary": {"covered_lines": 12, "num_statements": 12, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 0}, "missing_lines": [], "excluded_lines": []}, "TestShadcnInstaller.test_add_components_subprocess_error": {"executed_lines": [183, 187, 188, 190, 191], "summary": {"covered_lines": 5, "num_statements": 5, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 0}, "missing_lines": [], "excluded_lines": []}, "TestShadcnInstaller.test_add_components_npx_not_found": {"executed_lines": [196, 198, 199, 201, 202], "summary": {"covered_lines": 5, "num_statements": 5, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 0}, "missing_lines": [], "excluded_lines": []}, "TestShadcnInstaller.test_add_all_components_no_config": {"executed_lines": [206, 207, 209, 210], "summary": {"covered_lines": 4, "num_statements": 4, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 0}, "missing_lines": [], "excluded_lines": []}, "TestShadcnInstaller.test_add_all_components_dry_run": {"executed_lines": [214, 215, 217, 218, 219], "summary": {"covered_lines": 5, "num_statements": 5, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 0}, "missing_lines": [], "excluded_lines": []}, "TestShadcnInstaller.test_add_all_components_success": {"executed_lines": [224, 229, 230, 232, 233, 236, 237], "summary": {"covered_lines": 7, "num_statements": 7, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 0}, "missing_lines": [], "excluded_lines": []}, "TestShadcnInstaller.test_list_installed_no_config": {"executed_lines": [241, 242, 244, 245], "summary": {"covered_lines": 4, "num_statements": 4, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 0}, "missing_lines": [], "excluded_lines": []}, "TestShadcnInstaller.test_list_installed_empty": {"executed_lines": [249, 250, 252, 253], "summary": {"covered_lines": 4, "num_statements": 4, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 0}, "missing_lines": [], "excluded_lines": []}, "TestShadcnInstaller.test_list_installed_with_components": {"executed_lines": [257, 258, 259, 261, 262, 264, 265, 266], "summary": {"covered_lines": 8, "num_statements": 8, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 0}, "missing_lines": [], "excluded_lines": []}, "": {"executed_lines": [1, 3, 4, 5, 6, 8, 11, 12, 14, 17, 18, 20, 21, 44, 50, 55, 60, 65, 70, 76, 89, 95, 103, 111, 123, 146, 156, 157, 180, 181, 193, 194, 204, 212, 221, 222, 239, 247, 255], "summary": {"covered_lines": 37, "num_statements": 37, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 0}, "missing_lines": [], "excluded_lines": []}}, "classes": {"TestShadcnInstaller": {"executed_lines": [23, 24, 27, 28, 39, 40, 42, 46, 47, 48, 52, 53, 57, 58, 62, 63, 67, 68, 72, 73, 74, 78, 81, 82, 84, 85, 87, 91, 92, 93, 97, 98, 100, 101, 105, 106, 108, 109, 113, 114, 116, 117, 119, 120, 121, 125, 126, 128, 130, 131, 136, 138, 139, 140, 143, 144, 148, 149, 151, 152, 153, 154, 159, 165, 166, 168, 169, 170, 171, 174, 175, 176, 177, 178, 183, 187, 188, 190, 191, 196, 198, 199, 201, 202, 206, 207, 209, 210, 214, 215, 217, 218, 219, 224, 229, 230, 232, 233, 236, 237, 241, 242, 244, 245, 249, 250, 252, 253, 257, 258, 259, 261, 262, 264, 265, 266], "summary": {"covered_lines": 116, "num_statements": 116, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 0}, "missing_lines": [], "excluded_lines": []}, "": {"executed_lines": [1, 3, 4, 5, 6, 8, 11, 12, 14, 17, 18, 20, 21, 44, 50, 55, 60, 65, 70, 76, 89, 95, 103, 111, 123, 146, 156, 157, 180, 181, 193, 194, 204, 212, 221, 222, 239, 247, 255], "summary": {"covered_lines": 37, "num_statements": 37, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 0}, "missing_lines": [], "excluded_lines": []}}}, "tests/test_tailwind_config_gen.py": {"executed_lines": [1, 3, 5, 8, 9, 11, 14, 15, 17, 19, 20, 21, 23, 25, 26, 28, 30, 31, 32, 34, 36, 37, 39, 41, 42, 44, 46, 47, 48, 50, 52, 53, 55, 56, 57, 58, 59, 61, 63, 64, 66, 67, 69, 71, 72, 74, 75, 76, 78, 80, 81, 83, 85, 87, 88, 92, 94, 95, 96, 98, 100, 102, 103, 105, 106, 107, 109, 111, 112, 114, 116, 117, 118, 119, 120, 122, 124, 125, 129, 131, 132, 133, 135, 137, 138, 142, 144, 145, 146, 148, 150, 151, 155, 157, 158, 159, 161, 163, 164, 165, 167, 168, 170, 172, 173, 174, 176, 177, 179, 181, 182, 184, 185, 187, 189, 190, 192, 194, 196, 197, 199, 200, 201, 203, 205, 206, 208, 209, 211, 213, 214, 215, 217, 218, 220, 222, 223, 224, 226, 227, 229, 231, 232, 234, 236, 238, 239, 241, 243, 244, 246, 248, 251, 253, 254, 256, 258, 259, 261, 263, 264, 265, 267, 269, 270, 271, 273, 275, 276, 277, 279, 281, 283, 285, 286, 288, 290, 291, 298, 299, 300, 301, 302, 304, 305, 307, 310, 311, 312, 313, 314, 315, 317, 319, 320, 326, 327, 329, 330, 332, 334, 335, 336], "summary": {"covered_lines": 201, "num_statements": 201, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 0}, "missing_lines": [], "excluded_lines": [], "functions": {"TestTailwindConfigGenerator.test_init_default_typescript": {"executed_lines": [19, 20, 21], "summary": {"covered_lines": 3, "num_statements": 3, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 0}, "missing_lines": [], "excluded_lines": []}, "TestTailwindConfigGenerator.test_init_javascript": {"executed_lines": [25, 26], "summary": {"covered_lines": 2, "num_statements": 2, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 0}, "missing_lines": [], "excluded_lines": []}, "TestTailwindConfigGenerator.test_init_framework": {"executed_lines": [30, 31, 32], "summary": {"covered_lines": 3, "num_statements": 3, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 0}, "missing_lines": [], "excluded_lines": []}, "TestTailwindConfigGenerator.test_default_output_path_typescript": {"executed_lines": [36, 37], "summary": {"covered_lines": 2, "num_statements": 2, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 0}, "missing_lines": [], "excluded_lines": []}, "TestTailwindConfigGenerator.test_default_output_path_javascript": {"executed_lines": [41, 42], "summary": {"covered_lines": 2, "num_statements": 2, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 0}, "missing_lines": [], "excluded_lines": []}, "TestTailwindConfigGenerator.test_custom_output_path": {"executed_lines": [46, 47, 48], "summary": {"covered_lines": 3, "num_statements": 3, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 0}, "missing_lines": [], "excluded_lines": []}, "TestTailwindConfigGenerator.test_base_config_structure": {"executed_lines": [52, 53, 55, 56, 57, 58, 59], "summary": {"covered_lines": 7, "num_statements": 7, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 0}, "missing_lines": [], "excluded_lines": []}, "TestTailwindConfigGenerator.test_default_content_paths_react": {"executed_lines": [63, 64, 66, 67], "summary": {"covered_lines": 4, "num_statements": 4, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 0}, "missing_lines": [], "excluded_lines": []}, "TestTailwindConfigGenerator.test_default_content_paths_nextjs": {"executed_lines": [71, 72, 74, 75, 76], "summary": {"covered_lines": 5, "num_statements": 5, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 0}, "missing_lines": [], "excluded_lines": []}, "TestTailwindConfigGenerator.test_default_content_paths_vue": {"executed_lines": [80, 81, 83], "summary": {"covered_lines": 3, "num_statements": 3, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 0}, "missing_lines": [], "excluded_lines": []}, "TestTailwindConfigGenerator.test_add_colors": {"executed_lines": [87, 88, 92, 94, 95, 96], "summary": {"covered_lines": 6, "num_statements": 6, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 0}, "missing_lines": [], "excluded_lines": []}, "TestTailwindConfigGenerator.test_add_colors_multiple_times": {"executed_lines": [100, 102, 103, 105, 106, 107], "summary": {"covered_lines": 6, "num_statements": 6, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 0}, "missing_lines": [], "excluded_lines": []}, "TestTailwindConfigGenerator.test_add_color_palette": {"executed_lines": [111, 112, 114, 116, 117, 118, 119, 120], "summary": {"covered_lines": 8, "num_statements": 8, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 0}, "missing_lines": [], "excluded_lines": []}, "TestTailwindConfigGenerator.test_add_fonts": {"executed_lines": [124, 125, 129, 131, 132, 133], "summary": {"covered_lines": 6, "num_statements": 6, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 0}, "missing_lines": [], "excluded_lines": []}, "TestTailwindConfigGenerator.test_add_spacing": {"executed_lines": [137, 138, 142, 144, 145, 146], "summary": {"covered_lines": 6, "num_statements": 6, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 0}, "missing_lines": [], "excluded_lines": []}, "TestTailwindConfigGenerator.test_add_breakpoints": {"executed_lines": [150, 151, 155, 157, 158, 159], "summary": {"covered_lines": 6, "num_statements": 6, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 0}, "missing_lines": [], "excluded_lines": []}, "TestTailwindConfigGenerator.test_add_plugins": {"executed_lines": [163, 164, 165, 167, 168], "summary": {"covered_lines": 5, "num_statements": 5, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 0}, "missing_lines": [], "excluded_lines": []}, "TestTailwindConfigGenerator.test_add_plugins_no_duplicates": {"executed_lines": [172, 173, 174, 176, 177], "summary": {"covered_lines": 5, "num_statements": 5, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 0}, "missing_lines": [], "excluded_lines": []}, "TestTailwindConfigGenerator.test_recommend_plugins": {"executed_lines": [181, 182, 184, 185], "summary": {"covered_lines": 4, "num_statements": 4, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 0}, "missing_lines": [], "excluded_lines": []}, "TestTailwindConfigGenerator.test_recommend_plugins_nextjs": {"executed_lines": [189, 190, 192], "summary": {"covered_lines": 3, "num_statements": 3, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 0}, "missing_lines": [], "excluded_lines": []}, "TestTailwindConfigGenerator.test_generate_typescript_config": {"executed_lines": [196, 197, 199, 200, 201], "summary": {"covered_lines": 5, "num_statements": 5, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 0}, "missing_lines": [], "excluded_lines": []}, "TestTailwindConfigGenerator.test_generate_javascript_config": {"executed_lines": [205, 206, 208, 209], "summary": {"covered_lines": 4, "num_statements": 4, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 0}, "missing_lines": [], "excluded_lines": []}, "TestTailwindConfigGenerator.test_generate_config_with_colors": {"executed_lines": [213, 214, 215, 217, 218], "summary": {"covered_lines": 5, "num_statements": 5, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 0}, "missing_lines": [], "excluded_lines": []}, "TestTailwindConfigGenerator.test_generate_config_with_plugins": {"executed_lines": [222, 223, 224, 226, 227], "summary": {"covered_lines": 5, "num_statements": 5, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 0}, "missing_lines": [], "excluded_lines": []}, "TestTailwindConfigGenerator.test_validate_config_valid": {"executed_lines": [231, 232, 234], "summary": {"covered_lines": 3, "num_statements": 3, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 0}, "missing_lines": [], "excluded_lines": []}, "TestTailwindConfigGenerator.test_validate_config_no_content": {"executed_lines": [238, 239, 241, 243, 244], "summary": {"covered_lines": 5, "num_statements": 5, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 0}, "missing_lines": [], "excluded_lines": []}, "TestTailwindConfigGenerator.test_validate_config_empty_theme": {"executed_lines": [248, 251, 253, 254], "summary": {"covered_lines": 4, "num_statements": 4, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 0}, "missing_lines": [], "excluded_lines": []}, "TestTailwindConfigGenerator.test_write_config": {"executed_lines": [258, 259, 261, 263, 264, 265], "summary": {"covered_lines": 6, "num_statements": 6, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 0}, "missing_lines": [], "excluded_lines": []}, "TestTailwindConfigGenerator.test_write_config_creates_content": {"executed_lines": [269, 270, 271, 273, 275, 276, 277], "summary": {"covered_lines": 7, "num_statements": 7, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 0}, "missing_lines": [], "excluded_lines": []}, "TestTailwindConfigGenerator.test_write_config_invalid_path": {"executed_lines": [281, 283, 285, 286], "summary": {"covered_lines": 4, "num_statements": 4, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 0}, "missing_lines": [], "excluded_lines": []}, "TestTailwindConfigGenerator.test_full_configuration_typescript": {"executed_lines": [290, 291, 298, 299, 300, 301, 302, 304, 305, 307, 310, 311, 312, 313, 314, 315], "summary": {"covered_lines": 16, "num_statements": 16, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 0}, "missing_lines": [], "excluded_lines": []}, "TestTailwindConfigGenerator.test_full_configuration_javascript": {"executed_lines": [319, 320, 326, 327, 329, 330, 332, 334, 335, 336], "summary": {"covered_lines": 10, "num_statements": 10, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 0}, "missing_lines": [], "excluded_lines": []}, "": {"executed_lines": [1, 3, 5, 8, 9, 11, 14, 15, 17, 23, 28, 34, 39, 44, 50, 61, 69, 78, 85, 98, 109, 122, 135, 148, 161, 170, 179, 187, 194, 203, 211, 220, 229, 236, 246, 256, 267, 279, 288, 317], "summary": {"covered_lines": 38, "num_statements": 38, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 0}, "missing_lines": [], "excluded_lines": []}}, "classes": {"TestTailwindConfigGenerator": {"executed_lines": [19, 20, 21, 25, 26, 30, 31, 32, 36, 37, 41, 42, 46, 47, 48, 52, 53, 55, 56, 57, 58, 59, 63, 64, 66, 67, 71, 72, 74, 75, 76, 80, 81, 83, 87, 88, 92, 94, 95, 96, 100, 102, 103, 105, 106, 107, 111, 112, 114, 116, 117, 118, 119, 120, 124, 125, 129, 131, 132, 133, 137, 138, 142, 144, 145, 146, 150, 151, 155, 157, 158, 159, 163, 164, 165, 167, 168, 172, 173, 174, 176, 177, 181, 182, 184, 185, 189, 190, 192, 196, 197, 199, 200, 201, 205, 206, 208, 209, 213, 214, 215, 217, 218, 222, 223, 224, 226, 227, 231, 232, 234, 238, 239, 241, 243, 244, 248, 251, 253, 254, 258, 259, 261, 263, 264, 265, 269, 270, 271, 273, 275, 276, 277, 281, 283, 285, 286, 290, 291, 298, 299, 300, 301, 302, 304, 305, 307, 310, 311, 312, 313, 314, 315, 319, 320, 326, 327, 329, 330, 332, 334, 335, 336], "summary": {"covered_lines": 163, "num_statements": 163, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 0}, "missing_lines": [], "excluded_lines": []}, "": {"executed_lines": [1, 3, 5, 8, 9, 11, 14, 15, 17, 23, 28, 34, 39, 44, 50, 61, 69, 78, 85, 98, 109, 122, 135, 148, 161, 170, 179, 187, 194, 203, 211, 220, 229, 236, 246, 256, 267, 279, 288, 317], "summary": {"covered_lines": 38, "num_statements": 38, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 0}, "missing_lines": [], "excluded_lines": []}}}}, "totals": {"covered_lines": 514, "num_statements": 621, "percent_covered": 82.76972624798712, "percent_covered_display": "83", "missing_lines": 107, "excluded_lines": 0}} \ No newline at end of file diff --git a/skills/website-creator/ui-styling/scripts/tests/requirements.txt b/skills/website-creator/ui-styling/scripts/tests/requirements.txt new file mode 100644 index 0000000..3a0f66d --- /dev/null +++ b/skills/website-creator/ui-styling/scripts/tests/requirements.txt @@ -0,0 +1,3 @@ +pytest>=7.4.0 +pytest-cov>=4.1.0 +pytest-mock>=3.11.1 diff --git a/skills/website-creator/ui-styling/scripts/tests/test_shadcn_add.py b/skills/website-creator/ui-styling/scripts/tests/test_shadcn_add.py new file mode 100644 index 0000000..03c8f31 --- /dev/null +++ b/skills/website-creator/ui-styling/scripts/tests/test_shadcn_add.py @@ -0,0 +1,266 @@ +"""Tests for shadcn_add.py""" + +import json +import subprocess +from pathlib import Path +from unittest.mock import MagicMock, mock_open, patch + +import pytest + +# Add parent directory to path for imports +import sys +sys.path.insert(0, str(Path(__file__).parent.parent)) + +from shadcn_add import ShadcnInstaller + + +class TestShadcnInstaller: + """Test ShadcnInstaller class.""" + + @pytest.fixture + def temp_project(self, tmp_path): + """Create temporary project structure.""" + project_root = tmp_path / "test-project" + project_root.mkdir() + + # Create components.json + components_json = project_root / "components.json" + components_json.write_text( + json.dumps({ + "style": "new-york", + "aliases": { + "components": "@/components", + "utils": "@/lib/utils" + } + }) + ) + + # Create components directory + ui_dir = project_root / "components" / "ui" + ui_dir.mkdir(parents=True) + + return project_root + + def test_init_default_project_root(self): + """Test initialization with default project root.""" + installer = ShadcnInstaller() + assert installer.project_root == Path.cwd() + assert installer.dry_run is False + + def test_init_custom_project_root(self, tmp_path): + """Test initialization with custom project root.""" + installer = ShadcnInstaller(project_root=tmp_path) + assert installer.project_root == tmp_path + + def test_init_dry_run(self): + """Test initialization with dry run mode.""" + installer = ShadcnInstaller(dry_run=True) + assert installer.dry_run is True + + def test_check_shadcn_config_exists(self, temp_project): + """Test checking for existing shadcn config.""" + installer = ShadcnInstaller(project_root=temp_project) + assert installer.check_shadcn_config() is True + + def test_check_shadcn_config_not_exists(self, tmp_path): + """Test checking for non-existent shadcn config.""" + installer = ShadcnInstaller(project_root=tmp_path) + assert installer.check_shadcn_config() is False + + def test_get_installed_components_empty(self, temp_project): + """Test getting installed components when none exist.""" + installer = ShadcnInstaller(project_root=temp_project) + installed = installer.get_installed_components() + assert installed == [] + + def test_get_installed_components_with_files(self, temp_project): + """Test getting installed components when files exist.""" + ui_dir = temp_project / "components" / "ui" + + # Create component files + (ui_dir / "button.tsx").write_text("export const Button = () => {}") + (ui_dir / "card.tsx").write_text("export const Card = () => {}") + + installer = ShadcnInstaller(project_root=temp_project) + installed = installer.get_installed_components() + + assert sorted(installed) == ["button", "card"] + + def test_get_installed_components_no_config(self, tmp_path): + """Test getting installed components without config.""" + installer = ShadcnInstaller(project_root=tmp_path) + installed = installer.get_installed_components() + assert installed == [] + + def test_add_components_no_components(self, temp_project): + """Test adding components with empty list.""" + installer = ShadcnInstaller(project_root=temp_project) + success, message = installer.add_components([]) + + assert success is False + assert "No components specified" in message + + def test_add_components_no_config(self, tmp_path): + """Test adding components without shadcn config.""" + installer = ShadcnInstaller(project_root=tmp_path) + success, message = installer.add_components(["button"]) + + assert success is False + assert "not initialized" in message + + def test_add_components_already_installed(self, temp_project): + """Test adding components that are already installed.""" + ui_dir = temp_project / "components" / "ui" + (ui_dir / "button.tsx").write_text("export const Button = () => {}") + + installer = ShadcnInstaller(project_root=temp_project) + success, message = installer.add_components(["button"]) + + assert success is False + assert "already installed" in message + assert "button" in message + + def test_add_components_with_overwrite(self, temp_project): + """Test adding components with overwrite flag.""" + ui_dir = temp_project / "components" / "ui" + (ui_dir / "button.tsx").write_text("export const Button = () => {}") + + installer = ShadcnInstaller(project_root=temp_project) + + with patch("subprocess.run") as mock_run: + mock_run.return_value = MagicMock( + stdout="Component added successfully", + returncode=0 + ) + + success, message = installer.add_components(["button"], overwrite=True) + + assert success is True + assert "Successfully added" in message + mock_run.assert_called_once() + + # Verify --overwrite flag was passed + call_args = mock_run.call_args[0][0] + assert "--overwrite" in call_args + + def test_add_components_dry_run(self, temp_project): + """Test adding components in dry run mode.""" + installer = ShadcnInstaller(project_root=temp_project, dry_run=True) + success, message = installer.add_components(["button", "card"]) + + assert success is True + assert "Would run:" in message + assert "button" in message + assert "card" in message + + @patch("subprocess.run") + def test_add_components_success(self, mock_run, temp_project): + """Test successful component addition.""" + mock_run.return_value = MagicMock( + stdout="Components added successfully", + stderr="", + returncode=0 + ) + + installer = ShadcnInstaller(project_root=temp_project) + success, message = installer.add_components(["button", "card"]) + + assert success is True + assert "Successfully added" in message + assert "button" in message + assert "card" in message + + # Verify correct command was called + mock_run.assert_called_once() + call_args = mock_run.call_args[0][0] + assert call_args[:3] == ["npx", "shadcn@latest", "add"] + assert "button" in call_args + assert "card" in call_args + + @patch("subprocess.run") + def test_add_components_subprocess_error(self, mock_run, temp_project): + """Test component addition with subprocess error.""" + mock_run.side_effect = subprocess.CalledProcessError( + 1, "cmd", stderr="Error occurred" + ) + + installer = ShadcnInstaller(project_root=temp_project) + success, message = installer.add_components(["button"]) + + assert success is False + assert "Failed to add" in message + + @patch("subprocess.run") + def test_add_components_npx_not_found(self, mock_run, temp_project): + """Test component addition when npx is not found.""" + mock_run.side_effect = FileNotFoundError() + + installer = ShadcnInstaller(project_root=temp_project) + success, message = installer.add_components(["button"]) + + assert success is False + assert "npx not found" in message + + def test_add_all_components_no_config(self, tmp_path): + """Test adding all components without config.""" + installer = ShadcnInstaller(project_root=tmp_path) + success, message = installer.add_all_components() + + assert success is False + assert "not initialized" in message + + def test_add_all_components_dry_run(self, temp_project): + """Test adding all components in dry run mode.""" + installer = ShadcnInstaller(project_root=temp_project, dry_run=True) + success, message = installer.add_all_components() + + assert success is True + assert "Would run:" in message + assert "--all" in message + + @patch("subprocess.run") + def test_add_all_components_success(self, mock_run, temp_project): + """Test successful addition of all components.""" + mock_run.return_value = MagicMock( + stdout="All components added", + returncode=0 + ) + + installer = ShadcnInstaller(project_root=temp_project) + success, message = installer.add_all_components() + + assert success is True + assert "Successfully added all" in message + + # Verify --all flag was passed + call_args = mock_run.call_args[0][0] + assert "--all" in call_args + + def test_list_installed_no_config(self, tmp_path): + """Test listing installed components without config.""" + installer = ShadcnInstaller(project_root=tmp_path) + success, message = installer.list_installed() + + assert success is False + assert "not initialized" in message + + def test_list_installed_empty(self, temp_project): + """Test listing installed components when none exist.""" + installer = ShadcnInstaller(project_root=temp_project) + success, message = installer.list_installed() + + assert success is True + assert "No components installed" in message + + def test_list_installed_with_components(self, temp_project): + """Test listing installed components when they exist.""" + ui_dir = temp_project / "components" / "ui" + (ui_dir / "button.tsx").write_text("export const Button = () => {}") + (ui_dir / "card.tsx").write_text("export const Card = () => {}") + + installer = ShadcnInstaller(project_root=temp_project) + success, message = installer.list_installed() + + assert success is True + assert "button" in message + assert "card" in message diff --git a/skills/website-creator/ui-styling/scripts/tests/test_tailwind_config_gen.py b/skills/website-creator/ui-styling/scripts/tests/test_tailwind_config_gen.py new file mode 100644 index 0000000..a08414e --- /dev/null +++ b/skills/website-creator/ui-styling/scripts/tests/test_tailwind_config_gen.py @@ -0,0 +1,336 @@ +"""Tests for tailwind_config_gen.py""" + +from pathlib import Path + +import pytest + +# Add parent directory to path for imports +import sys +sys.path.insert(0, str(Path(__file__).parent.parent)) + +from tailwind_config_gen import TailwindConfigGenerator + + +class TestTailwindConfigGenerator: + """Test TailwindConfigGenerator class.""" + + def test_init_default_typescript(self): + """Test initialization with default settings.""" + generator = TailwindConfigGenerator() + assert generator.typescript is True + assert generator.framework == "react" + + def test_init_javascript(self): + """Test initialization for JavaScript config.""" + generator = TailwindConfigGenerator(typescript=False) + assert generator.typescript is False + + def test_init_framework(self): + """Test initialization with different frameworks.""" + for framework in ["react", "vue", "svelte", "nextjs"]: + generator = TailwindConfigGenerator(framework=framework) + assert generator.framework == framework + + def test_default_output_path_typescript(self): + """Test default output path for TypeScript.""" + generator = TailwindConfigGenerator(typescript=True) + assert generator.output_path.name == "tailwind.config.ts" + + def test_default_output_path_javascript(self): + """Test default output path for JavaScript.""" + generator = TailwindConfigGenerator(typescript=False) + assert generator.output_path.name == "tailwind.config.js" + + def test_custom_output_path(self, tmp_path): + """Test custom output path.""" + custom_path = tmp_path / "custom-config.ts" + generator = TailwindConfigGenerator(output_path=custom_path) + assert generator.output_path == custom_path + + def test_base_config_structure(self): + """Test base configuration structure.""" + generator = TailwindConfigGenerator() + config = generator.config + + assert "darkMode" in config + assert "content" in config + assert "theme" in config + assert "plugins" in config + assert "extend" in config["theme"] + + def test_default_content_paths_react(self): + """Test default content paths for React.""" + generator = TailwindConfigGenerator(framework="react") + paths = generator.config["content"] + + assert any("src/**/*.{js,jsx,ts,tsx}" in p for p in paths) + assert any("index.html" in p for p in paths) + + def test_default_content_paths_nextjs(self): + """Test default content paths for Next.js.""" + generator = TailwindConfigGenerator(framework="nextjs") + paths = generator.config["content"] + + assert any("app/**" in p for p in paths) + assert any("pages/**" in p for p in paths) + assert any("components/**" in p for p in paths) + + def test_default_content_paths_vue(self): + """Test default content paths for Vue.""" + generator = TailwindConfigGenerator(framework="vue") + paths = generator.config["content"] + + assert any("vue" in p for p in paths) + + def test_add_colors(self): + """Test adding custom colors.""" + generator = TailwindConfigGenerator() + colors = { + "brand": "#3b82f6", + "accent": "#8b5cf6" + } + generator.add_colors(colors) + + assert "colors" in generator.config["theme"]["extend"] + assert generator.config["theme"]["extend"]["colors"]["brand"] == "#3b82f6" + assert generator.config["theme"]["extend"]["colors"]["accent"] == "#8b5cf6" + + def test_add_colors_multiple_times(self): + """Test adding colors multiple times.""" + generator = TailwindConfigGenerator() + + generator.add_colors({"brand": "#3b82f6"}) + generator.add_colors({"accent": "#8b5cf6"}) + + colors = generator.config["theme"]["extend"]["colors"] + assert "brand" in colors + assert "accent" in colors + + def test_add_color_palette(self): + """Test adding full color palette.""" + generator = TailwindConfigGenerator() + generator.add_color_palette("brand", "#3b82f6") + + brand = generator.config["theme"]["extend"]["colors"]["brand"] + + assert isinstance(brand, dict) + assert "50" in brand + assert "500" in brand + assert "950" in brand + assert "var(--color-brand" in brand["500"] + + def test_add_fonts(self): + """Test adding custom fonts.""" + generator = TailwindConfigGenerator() + fonts = { + "sans": ["Inter", "system-ui", "sans-serif"], + "display": ["Playfair Display", "serif"] + } + generator.add_fonts(fonts) + + font_family = generator.config["theme"]["extend"]["fontFamily"] + assert font_family["sans"] == ["Inter", "system-ui", "sans-serif"] + assert font_family["display"] == ["Playfair Display", "serif"] + + def test_add_spacing(self): + """Test adding custom spacing.""" + generator = TailwindConfigGenerator() + spacing = { + "18": "4.5rem", + "navbar": "4rem" + } + generator.add_spacing(spacing) + + spacing_config = generator.config["theme"]["extend"]["spacing"] + assert spacing_config["18"] == "4.5rem" + assert spacing_config["navbar"] == "4rem" + + def test_add_breakpoints(self): + """Test adding custom breakpoints.""" + generator = TailwindConfigGenerator() + breakpoints = { + "3xl": "1920px", + "tablet": "768px" + } + generator.add_breakpoints(breakpoints) + + screens = generator.config["theme"]["extend"]["screens"] + assert screens["3xl"] == "1920px" + assert screens["tablet"] == "768px" + + def test_add_plugins(self): + """Test adding plugins.""" + generator = TailwindConfigGenerator() + plugins = ["@tailwindcss/typography", "@tailwindcss/forms"] + generator.add_plugins(plugins) + + assert "@tailwindcss/typography" in generator.config["plugins"] + assert "@tailwindcss/forms" in generator.config["plugins"] + + def test_add_plugins_no_duplicates(self): + """Test that adding same plugin twice doesn't duplicate.""" + generator = TailwindConfigGenerator() + generator.add_plugins(["@tailwindcss/typography"]) + generator.add_plugins(["@tailwindcss/typography"]) + + count = generator.config["plugins"].count("@tailwindcss/typography") + assert count == 1 + + def test_recommend_plugins(self): + """Test plugin recommendations.""" + generator = TailwindConfigGenerator() + recommendations = generator.recommend_plugins() + + assert isinstance(recommendations, list) + assert "tailwindcss-animate" in recommendations + + def test_recommend_plugins_nextjs(self): + """Test plugin recommendations for Next.js.""" + generator = TailwindConfigGenerator(framework="nextjs") + recommendations = generator.recommend_plugins() + + assert "@tailwindcss/typography" in recommendations + + def test_generate_typescript_config(self): + """Test generating TypeScript configuration.""" + generator = TailwindConfigGenerator(typescript=True) + config = generator.generate_config_string() + + assert "import type { Config } from 'tailwindcss'" in config + assert "const config: Config" in config + assert "export default config" in config + + def test_generate_javascript_config(self): + """Test generating JavaScript configuration.""" + generator = TailwindConfigGenerator(typescript=False) + config = generator.generate_config_string() + + assert "module.exports" in config + assert "@type" in config + + def test_generate_config_with_colors(self): + """Test generating config with custom colors.""" + generator = TailwindConfigGenerator() + generator.add_colors({"brand": "#3b82f6"}) + config = generator.generate_config_string() + + assert "colors" in config + assert "brand" in config + + def test_generate_config_with_plugins(self): + """Test generating config with plugins.""" + generator = TailwindConfigGenerator() + generator.add_plugins(["tailwindcss-animate"]) + config = generator.generate_config_string() + + assert "plugins:" in config + assert "require('tailwindcss-animate')" in config + + def test_validate_config_valid(self): + """Test validating valid configuration.""" + generator = TailwindConfigGenerator() + valid, message = generator.validate_config() + + assert valid is True + + def test_validate_config_no_content(self): + """Test validating config with no content paths.""" + generator = TailwindConfigGenerator() + generator.config["content"] = [] + + valid, message = generator.validate_config() + + assert valid is False + assert "No content paths" in message + + def test_validate_config_empty_theme(self): + """Test validating config with empty theme extensions.""" + generator = TailwindConfigGenerator() + # Default has empty theme.extend + + valid, message = generator.validate_config() + + assert valid is True + assert "Warning" in message + + def test_write_config(self, tmp_path): + """Test writing configuration to file.""" + output_path = tmp_path / "tailwind.config.ts" + generator = TailwindConfigGenerator(output_path=output_path) + + success, message = generator.write_config() + + assert success is True + assert output_path.exists() + assert "written to" in message + + def test_write_config_creates_content(self, tmp_path): + """Test that written config contains expected content.""" + output_path = tmp_path / "tailwind.config.ts" + generator = TailwindConfigGenerator(output_path=output_path) + generator.add_colors({"brand": "#3b82f6"}) + + generator.write_config() + + content = output_path.read_text() + assert "import type { Config }" in content + assert "brand" in content + + def test_write_config_invalid_path(self): + """Test writing config to invalid path.""" + generator = TailwindConfigGenerator(output_path=Path("/invalid/path/config.ts")) + + success, message = generator.write_config() + + assert success is False + assert "Failed to write" in message + + def test_full_configuration_typescript(self, tmp_path): + """Test generating complete TypeScript configuration.""" + output_path = tmp_path / "tailwind.config.ts" + generator = TailwindConfigGenerator( + typescript=True, + framework="nextjs", + output_path=output_path + ) + + # Add various customizations + generator.add_colors({"brand": "#3b82f6", "accent": "#8b5cf6"}) + generator.add_fonts({"sans": ["Inter", "sans-serif"]}) + generator.add_spacing({"navbar": "4rem"}) + generator.add_breakpoints({"3xl": "1920px"}) + generator.add_plugins(["tailwindcss-animate"]) + + success, _ = generator.write_config() + assert success is True + + content = output_path.read_text() + + # Verify all customizations are present + assert "brand" in content + assert "accent" in content + assert "Inter" in content + assert "navbar" in content + assert "3xl" in content + assert "tailwindcss-animate" in content + + def test_full_configuration_javascript(self, tmp_path): + """Test generating complete JavaScript configuration.""" + output_path = tmp_path / "tailwind.config.js" + generator = TailwindConfigGenerator( + typescript=False, + framework="react", + output_path=output_path + ) + + generator.add_colors({"primary": "#3b82f6"}) + generator.add_plugins(["@tailwindcss/forms"]) + + success, _ = generator.write_config() + assert success is True + + content = output_path.read_text() + + assert "module.exports" in content + assert "primary" in content + assert "@tailwindcss/forms" in content diff --git a/skills/website-creator/ui-ux-pro-max/SKILL.md b/skills/website-creator/ui-ux-pro-max/SKILL.md new file mode 100644 index 0000000..6b2e0ea --- /dev/null +++ b/skills/website-creator/ui-ux-pro-max/SKILL.md @@ -0,0 +1,659 @@ +--- +name: ui-ux-pro-max +description: "UI/UX design intelligence for web and mobile. Includes 50+ styles, 161 color palettes, 57 font pairings, 161 product types, 99 UX guidelines, and 25 chart types across 10 stacks (React, Next.js, Vue, Svelte, SwiftUI, React Native, Flutter, Tailwind, shadcn/ui, and HTML/CSS). Actions: plan, build, create, design, implement, review, fix, improve, optimize, enhance, refactor, and check UI/UX code. Projects: website, landing page, dashboard, admin panel, e-commerce, SaaS, portfolio, blog, and mobile app. Elements: button, modal, navbar, sidebar, card, table, form, and chart. Styles: glassmorphism, claymorphism, minimalism, brutalism, neumorphism, bento grid, dark mode, responsive, skeuomorphism, and flat design. Topics: color systems, accessibility, animation, layout, typography, font pairing, spacing, interaction states, shadow, and gradient. Integrations: shadcn/ui MCP for component search and examples." +--- + +# UI/UX Pro Max - Design Intelligence + +Comprehensive design guide for web and mobile applications. Contains 50+ styles, 161 color palettes, 57 font pairings, 161 product types with reasoning rules, 99 UX guidelines, and 25 chart types across 10 technology stacks. Searchable database with priority-based recommendations. + +## When to Apply + +This Skill should be used when the task involves **UI structure, visual design decisions, interaction patterns, or user experience quality control**. + +### Must Use + +This Skill must be invoked in the following situations: + +- Designing new pages (Landing Page, Dashboard, Admin, SaaS, Mobile App) +- Creating or refactoring UI components (buttons, modals, forms, tables, charts, etc.) +- Choosing color schemes, typography systems, spacing standards, or layout systems +- Reviewing UI code for user experience, accessibility, or visual consistency +- Implementing navigation structures, animations, or responsive behavior +- Making product-level design decisions (style, information hierarchy, brand expression) +- Improving perceived quality, clarity, or usability of interfaces + +### Recommended + +This Skill is recommended in the following situations: + +- UI looks "not professional enough" but the reason is unclear +- Receiving feedback on usability or experience +- Pre-launch UI quality optimization +- Aligning cross-platform design (Web / iOS / Android) +- Building design systems or reusable component libraries + +### Skip + +This Skill is not needed in the following situations: + +- Pure backend logic development +- Only involving API or database design +- Performance optimization unrelated to the interface +- Infrastructure or DevOps work +- Non-visual scripts or automation tasks + +**Decision criteria**: If the task will change how a feature **looks, feels, moves, or is interacted with**, this Skill should be used. + +## Rule Categories by Priority + +*For human/AI reference: follow priority 1→10 to decide which rule category to focus on first; use `--domain ` to query details when needed. Scripts do not read this table.* + +| Priority | Category | Impact | Domain | Key Checks (Must Have) | Anti-Patterns (Avoid) | +|----------|----------|--------|--------|------------------------|------------------------| +| 1 | Accessibility | CRITICAL | `ux` | Contrast 4.5:1, Alt text, Keyboard nav, Aria-labels | Removing focus rings, Icon-only buttons without labels | +| 2 | Touch & Interaction | CRITICAL | `ux` | Min size 44×44px, 8px+ spacing, Loading feedback | Reliance on hover only, Instant state changes (0ms) | +| 3 | Performance | HIGH | `ux` | WebP/AVIF, Lazy loading, Reserve space (CLS < 0.1) | Layout thrashing, Cumulative Layout Shift | +| 4 | Style Selection | HIGH | `style`, `product` | Match product type, Consistency, SVG icons (no emoji) | Mixing flat & skeuomorphic randomly, Emoji as icons | +| 5 | Layout & Responsive | HIGH | `ux` | Mobile-first breakpoints, Viewport meta, No horizontal scroll | Horizontal scroll, Fixed px container widths, Disable zoom | +| 6 | Typography & Color | MEDIUM | `typography`, `color` | Base 16px, Line-height 1.5, Semantic color tokens | Text < 12px body, Gray-on-gray, Raw hex in components | +| 7 | Animation | MEDIUM | `ux` | Duration 150–300ms, Motion conveys meaning, Spatial continuity | Decorative-only animation, Animating width/height, No reduced-motion | +| 8 | Forms & Feedback | MEDIUM | `ux` | Visible labels, Error near field, Helper text, Progressive disclosure | Placeholder-only label, Errors only at top, Overwhelm upfront | +| 9 | Navigation Patterns | HIGH | `ux` | Predictable back, Bottom nav ≤5, Deep linking | Overloaded nav, Broken back behavior, No deep links | +| 10 | Charts & Data | LOW | `chart` | Legends, Tooltips, Accessible colors | Relying on color alone to convey meaning | + +## Quick Reference + +### 1. Accessibility (CRITICAL) + +- `color-contrast` - Minimum 4.5:1 ratio for normal text (large text 3:1); Material Design +- `focus-states` - Visible focus rings on interactive elements (2–4px; Apple HIG, MD) +- `alt-text` - Descriptive alt text for meaningful images +- `aria-labels` - aria-label for icon-only buttons; accessibilityLabel in native (Apple HIG) +- `keyboard-nav` - Tab order matches visual order; full keyboard support (Apple HIG) +- `form-labels` - Use label with for attribute +- `skip-links` - Skip to main content for keyboard users +- `heading-hierarchy` - Sequential h1→h6, no level skip +- `color-not-only` - Don't convey info by color alone (add icon/text) +- `dynamic-type` - Support system text scaling; avoid truncation as text grows (Apple Dynamic Type, MD) +- `reduced-motion` - Respect prefers-reduced-motion; reduce/disable animations when requested (Apple Reduced Motion API, MD) +- `voiceover-sr` - Meaningful accessibilityLabel/accessibilityHint; logical reading order for VoiceOver/screen readers (Apple HIG, MD) +- `escape-routes` - Provide cancel/back in modals and multi-step flows (Apple HIG) +- `keyboard-shortcuts` - Preserve system and a11y shortcuts; offer keyboard alternatives for drag-and-drop (Apple HIG) + +### 2. Touch & Interaction (CRITICAL) + +- `touch-target-size` - Min 44×44pt (Apple) / 48×48dp (Material); extend hit area beyond visual bounds if needed +- `touch-spacing` - Minimum 8px/8dp gap between touch targets (Apple HIG, MD) +- `hover-vs-tap` - Use click/tap for primary interactions; don't rely on hover alone +- `loading-buttons` - Disable button during async operations; show spinner or progress +- `error-feedback` - Clear error messages near problem +- `cursor-pointer` - Add cursor-pointer to clickable elements (Web) +- `gesture-conflicts` - Avoid horizontal swipe on main content; prefer vertical scroll +- `tap-delay` - Use touch-action: manipulation to reduce 300ms delay (Web) +- `standard-gestures` - Use platform standard gestures consistently; don't redefine (e.g. swipe-back, pinch-zoom) (Apple HIG) +- `system-gestures` - Don't block system gestures (Control Center, back swipe, etc.) (Apple HIG) +- `press-feedback` - Visual feedback on press (ripple/highlight; MD state layers) +- `haptic-feedback` - Use haptic for confirmations and important actions; avoid overuse (Apple HIG) +- `gesture-alternative` - Don't rely on gesture-only interactions; always provide visible controls for critical actions +- `safe-area-awareness` - Keep primary touch targets away from notch, Dynamic Island, gesture bar and screen edges +- `no-precision-required` - Avoid requiring pixel-perfect taps on small icons or thin edges +- `swipe-clarity` - Swipe actions must show clear affordance or hint (chevron, label, tutorial) +- `drag-threshold` - Use a movement threshold before starting drag to avoid accidental drags + +### 3. Performance (HIGH) + +- `image-optimization` - Use WebP/AVIF, responsive images (srcset/sizes), lazy load non-critical assets +- `image-dimension` - Declare width/height or use aspect-ratio to prevent layout shift (Core Web Vitals: CLS) +- `font-loading` - Use font-display: swap/optional to avoid invisible text (FOIT); reserve space to reduce layout shift (MD) +- `font-preload` - Preload only critical fonts; avoid overusing preload on every variant +- `critical-css` - Prioritize above-the-fold CSS (inline critical CSS or early-loaded stylesheet) +- `lazy-loading` - Lazy load non-hero components via dynamic import / route-level splitting +- `bundle-splitting` - Split code by route/feature (React Suspense / Next.js dynamic) to reduce initial load and TTI +- `third-party-scripts` - Load third-party scripts async/defer; audit and remove unnecessary ones (MD) +- `reduce-reflows` - Avoid frequent layout reads/writes; batch DOM reads then writes +- `content-jumping` - Reserve space for async content to avoid layout jumps (Core Web Vitals: CLS) +- `lazy-load-below-fold` - Use loading="lazy" for below-the-fold images and heavy media +- `virtualize-lists` - Virtualize lists with 50+ items to improve memory efficiency and scroll performance +- `main-thread-budget` - Keep per-frame work under ~16ms for 60fps; move heavy tasks off main thread (HIG, MD) +- `progressive-loading` - Use skeleton screens / shimmer instead of long blocking spinners for >1s operations (Apple HIG) +- `input-latency` - Keep input latency under ~100ms for taps/scrolls (Material responsiveness standard) +- `tap-feedback-speed` - Provide visual feedback within 100ms of tap (Apple HIG) +- `debounce-throttle` - Use debounce/throttle for high-frequency events (scroll, resize, input) +- `offline-support` - Provide offline state messaging and basic fallback (PWA / mobile) +- `network-fallback` - Offer degraded modes for slow networks (lower-res images, fewer animations) + +### 4. Style Selection (HIGH) + +- `style-match` - Match style to product type (use `--design-system` for recommendations) +- `consistency` - Use same style across all pages +- `no-emoji-icons` - Use SVG icons (Heroicons, Lucide), not emojis +- `color-palette-from-product` - Choose palette from product/industry (search `--domain color`) +- `effects-match-style` - Shadows, blur, radius aligned with chosen style (glass / flat / clay etc.) +- `platform-adaptive` - Respect platform idioms (iOS HIG vs Material): navigation, controls, typography, motion +- `state-clarity` - Make hover/pressed/disabled states visually distinct while staying on-style (Material state layers) +- `elevation-consistent` - Use a consistent elevation/shadow scale for cards, sheets, modals; avoid random shadow values +- `dark-mode-pairing` - Design light/dark variants together to keep brand, contrast, and style consistent +- `icon-style-consistent` - Use one icon set/visual language (stroke width, corner radius) across the product +- `system-controls` - Prefer native/system controls over fully custom ones; only customize when branding requires it (Apple HIG) +- `blur-purpose` - Use blur to indicate background dismissal (modals, sheets), not as decoration (Apple HIG) +- `primary-action` - Each screen should have only one primary CTA; secondary actions visually subordinate (Apple HIG) + +### 5. Layout & Responsive (HIGH) + +- `viewport-meta` - width=device-width initial-scale=1 (never disable zoom) +- `mobile-first` - Design mobile-first, then scale up to tablet and desktop +- `breakpoint-consistency` - Use systematic breakpoints (e.g. 375 / 768 / 1024 / 1440) +- `readable-font-size` - Minimum 16px body text on mobile (avoids iOS auto-zoom) +- `line-length-control` - Mobile 35–60 chars per line; desktop 60–75 chars +- `horizontal-scroll` - No horizontal scroll on mobile; ensure content fits viewport width +- `spacing-scale` - Use 4pt/8dp incremental spacing system (Material Design) +- `touch-density` - Keep component spacing comfortable for touch: not cramped, not causing mis-taps +- `container-width` - Consistent max-width on desktop (max-w-6xl / 7xl) +- `z-index-management` - Define layered z-index scale (e.g. 0 / 10 / 20 / 40 / 100 / 1000) +- `fixed-element-offset` - Fixed navbar/bottom bar must reserve safe padding for underlying content +- `scroll-behavior` - Avoid nested scroll regions that interfere with the main scroll experience +- `viewport-units` - Prefer min-h-dvh over 100vh on mobile +- `orientation-support` - Keep layout readable and operable in landscape mode +- `content-priority` - Show core content first on mobile; fold or hide secondary content +- `visual-hierarchy` - Establish hierarchy via size, spacing, contrast — not color alone + +### 6. Typography & Color (MEDIUM) + +- `line-height` - Use 1.5-1.75 for body text +- `line-length` - Limit to 65-75 characters per line +- `font-pairing` - Match heading/body font personalities +- `font-scale` - Consistent type scale (e.g. 12 14 16 18 24 32) +- `contrast-readability` - Darker text on light backgrounds (e.g. slate-900 on white) +- `text-styles-system` - Use platform type system: iOS 11 Dynamic Type styles / Material 5 type roles (display, headline, title, body, label) (HIG, MD) +- `weight-hierarchy` - Use font-weight to reinforce hierarchy: Bold headings (600–700), Regular body (400), Medium labels (500) (MD) +- `color-semantic` - Define semantic color tokens (primary, secondary, error, surface, on-surface) not raw hex in components (Material color system) +- `color-dark-mode` - Dark mode uses desaturated / lighter tonal variants, not inverted colors; test contrast separately (HIG, MD) +- `color-accessible-pairs` - Foreground/background pairs must meet 4.5:1 (AA) or 7:1 (AAA); use tools to verify (WCAG, MD) +- `color-not-decorative-only` - Functional color (error red, success green) must include icon/text; avoid color-only meaning (HIG, MD) +- `truncation-strategy` - Prefer wrapping over truncation; when truncating use ellipsis and provide full text via tooltip/expand (Apple HIG) +- `letter-spacing` - Respect default letter-spacing per platform; avoid tight tracking on body text (HIG, MD) +- `number-tabular` - Use tabular/monospaced figures for data columns, prices, and timers to prevent layout shift +- `whitespace-balance` - Use whitespace intentionally to group related items and separate sections; avoid visual clutter (Apple HIG) + +### 7. Animation (MEDIUM) + +- `duration-timing` - Use 150–300ms for micro-interactions; complex transitions ≤400ms; avoid >500ms (MD) +- `transform-performance` - Use transform/opacity only; avoid animating width/height/top/left +- `loading-states` - Show skeleton or progress indicator when loading exceeds 300ms +- `excessive-motion` - Animate 1-2 key elements per view max +- `easing` - Use ease-out for entering, ease-in for exiting; avoid linear for UI transitions +- `motion-meaning` - Every animation must express a cause-effect relationship, not just be decorative (Apple HIG) +- `state-transition` - State changes (hover / active / expanded / collapsed / modal) should animate smoothly, not snap +- `continuity` - Page/screen transitions should maintain spatial continuity (shared element, directional slide) (Apple HIG) +- `parallax-subtle` - Use parallax sparingly; must respect reduced-motion and not cause disorientation (Apple HIG) +- `spring-physics` - Prefer spring/physics-based curves over linear or cubic-bezier for natural feel (Apple HIG fluid animations) +- `exit-faster-than-enter` - Exit animations shorter than enter (~60–70% of enter duration) to feel responsive (MD motion) +- `stagger-sequence` - Stagger list/grid item entrance by 30–50ms per item; avoid all-at-once or too-slow reveals (MD) +- `shared-element-transition` - Use shared element / hero transitions for visual continuity between screens (MD, HIG) +- `interruptible` - Animations must be interruptible; user tap/gesture cancels in-progress animation immediately (Apple HIG) +- `no-blocking-animation` - Never block user input during an animation; UI must stay interactive (Apple HIG) +- `fade-crossfade` - Use crossfade for content replacement within the same container (MD) +- `scale-feedback` - Subtle scale (0.95–1.05) on press for tappable cards/buttons; restore on release (HIG, MD) +- `gesture-feedback` - Drag, swipe, and pinch must provide real-time visual response tracking the finger (MD Motion) +- `hierarchy-motion` - Use translate/scale direction to express hierarchy: enter from below = deeper, exit upward = back (MD) +- `motion-consistency` - Unify duration/easing tokens globally; all animations share the same rhythm and feel +- `opacity-threshold` - Fading elements should not linger below opacity 0.2; either fade fully or remain visible +- `modal-motion` - Modals/sheets should animate from their trigger source (scale+fade or slide-in) for spatial context (HIG, MD) +- `navigation-direction` - Forward navigation animates left/up; backward animates right/down — keep direction logically consistent (HIG) +- `layout-shift-avoid` - Animations must not cause layout reflow or CLS; use transform for position changes + +### 8. Forms & Feedback (MEDIUM) + +- `input-labels` - Visible label per input (not placeholder-only) +- `error-placement` - Show error below the related field +- `submit-feedback` - Loading then success/error state on submit +- `required-indicators` - Mark required fields (e.g. asterisk) +- `empty-states` - Helpful message and action when no content +- `toast-dismiss` - Auto-dismiss toasts in 3-5s +- `confirmation-dialogs` - Confirm before destructive actions +- `input-helper-text` - Provide persistent helper text below complex inputs, not just placeholder (Material Design) +- `disabled-states` - Disabled elements use reduced opacity (0.38–0.5) + cursor change + semantic attribute (MD) +- `progressive-disclosure` - Reveal complex options progressively; don't overwhelm users upfront (Apple HIG) +- `inline-validation` - Validate on blur (not keystroke); show error only after user finishes input (MD) +- `input-type-keyboard` - Use semantic input types (email, tel, number) to trigger the correct mobile keyboard (HIG, MD) +- `password-toggle` - Provide show/hide toggle for password fields (MD) +- `autofill-support` - Use autocomplete / textContentType attributes so the system can autofill (HIG, MD) +- `undo-support` - Allow undo for destructive or bulk actions (e.g. "Undo delete" toast) (Apple HIG) +- `success-feedback` - Confirm completed actions with brief visual feedback (checkmark, toast, color flash) (MD) +- `error-recovery` - Error messages must include a clear recovery path (retry, edit, help link) (HIG, MD) +- `multi-step-progress` - Multi-step flows show step indicator or progress bar; allow back navigation (MD) +- `form-autosave` - Long forms should auto-save drafts to prevent data loss on accidental dismissal (Apple HIG) +- `sheet-dismiss-confirm` - Confirm before dismissing a sheet/modal with unsaved changes (Apple HIG) +- `error-clarity` - Error messages must state cause + how to fix (not just "Invalid input") (HIG, MD) +- `field-grouping` - Group related fields logically (fieldset/legend or visual grouping) (MD) +- `read-only-distinction` - Read-only state should be visually and semantically different from disabled (MD) +- `focus-management` - After submit error, auto-focus the first invalid field (WCAG, MD) +- `error-summary` - For multiple errors, show summary at top with anchor links to each field (WCAG) +- `touch-friendly-input` - Mobile input height ≥44px to meet touch target requirements (Apple HIG) +- `destructive-emphasis` - Destructive actions use semantic danger color (red) and are visually separated from primary actions (HIG, MD) +- `toast-accessibility` - Toasts must not steal focus; use aria-live="polite" for screen reader announcement (WCAG) +- `aria-live-errors` - Form errors use aria-live region or role="alert" to notify screen readers (WCAG) +- `contrast-feedback` - Error and success state colors must meet 4.5:1 contrast ratio (WCAG, MD) +- `timeout-feedback` - Request timeout must show clear feedback with retry option (MD) + +### 9. Navigation Patterns (HIGH) + +- `bottom-nav-limit` - Bottom navigation max 5 items; use labels with icons (Material Design) +- `drawer-usage` - Use drawer/sidebar for secondary navigation, not primary actions (Material Design) +- `back-behavior` - Back navigation must be predictable and consistent; preserve scroll/state (Apple HIG, MD) +- `deep-linking` - All key screens must be reachable via deep link / URL for sharing and notifications (Apple HIG, MD) +- `tab-bar-ios` - iOS: use bottom Tab Bar for top-level navigation (Apple HIG) +- `top-app-bar-android` - Android: use Top App Bar with navigation icon for primary structure (Material Design) +- `nav-label-icon` - Navigation items must have both icon and text label; icon-only nav harms discoverability (MD) +- `nav-state-active` - Current location must be visually highlighted (color, weight, indicator) in navigation (HIG, MD) +- `nav-hierarchy` - Primary nav (tabs/bottom bar) vs secondary nav (drawer/settings) must be clearly separated (MD) +- `modal-escape` - Modals and sheets must offer a clear close/dismiss affordance; swipe-down to dismiss on mobile (Apple HIG) +- `search-accessible` - Search must be easily reachable (top bar or tab); provide recent/suggested queries (MD) +- `breadcrumb-web` - Web: use breadcrumbs for 3+ level deep hierarchies to aid orientation (MD) +- `state-preservation` - Navigating back must restore previous scroll position, filter state, and input (HIG, MD) +- `gesture-nav-support` - Support system gesture navigation (iOS swipe-back, Android predictive back) without conflict (HIG, MD) +- `tab-badge` - Use badges on nav items sparingly to indicate unread/pending; clear after user visits (HIG, MD) +- `overflow-menu` - When actions exceed available space, use overflow/more menu instead of cramming (MD) +- `bottom-nav-top-level` - Bottom nav is for top-level screens only; never nest sub-navigation inside it (MD) +- `adaptive-navigation` - Large screens (≥1024px) prefer sidebar; small screens use bottom/top nav (Material Adaptive) +- `back-stack-integrity` - Never silently reset the navigation stack or unexpectedly jump to home (HIG, MD) +- `navigation-consistency` - Navigation placement must stay the same across all pages; don't change by page type +- `avoid-mixed-patterns` - Don't mix Tab + Sidebar + Bottom Nav at the same hierarchy level +- `modal-vs-navigation` - Modals must not be used for primary navigation flows; they break the user's path (HIG) +- `focus-on-route-change` - After page transition, move focus to main content region for screen reader users (WCAG) +- `persistent-nav` - Core navigation must remain reachable from deep pages; don't hide it entirely in sub-flows (HIG, MD) +- `destructive-nav-separation` - Dangerous actions (delete account, logout) must be visually and spatially separated from normal nav items (HIG, MD) +- `empty-nav-state` - When a nav destination is unavailable, explain why instead of silently hiding it (MD) + +### 10. Charts & Data (LOW) + +- `chart-type` - Match chart type to data type (trend → line, comparison → bar, proportion → pie/donut) +- `color-guidance` - Use accessible color palettes; avoid red/green only pairs for colorblind users (WCAG, MD) +- `data-table` - Provide table alternative for accessibility; charts alone are not screen-reader friendly (WCAG) +- `pattern-texture` - Supplement color with patterns, textures, or shapes so data is distinguishable without color (WCAG, MD) +- `legend-visible` - Always show legend; position near the chart, not detached below a scroll fold (MD) +- `tooltip-on-interact` - Provide tooltips/data labels on hover (Web) or tap (mobile) showing exact values (HIG, MD) +- `axis-labels` - Label axes with units and readable scale; avoid truncated or rotated labels on mobile +- `responsive-chart` - Charts must reflow or simplify on small screens (e.g. horizontal bar instead of vertical, fewer ticks) +- `empty-data-state` - Show meaningful empty state when no data exists ("No data yet" + guidance), not a blank chart (MD) +- `loading-chart` - Use skeleton or shimmer placeholder while chart data loads; don't show an empty axis frame +- `animation-optional` - Chart entrance animations must respect prefers-reduced-motion; data should be readable immediately (HIG) +- `large-dataset` - For 1000+ data points, aggregate or sample; provide drill-down for detail instead of rendering all (MD) +- `number-formatting` - Use locale-aware formatting for numbers, dates, currencies on axes and labels (HIG, MD) +- `touch-target-chart` - Interactive chart elements (points, segments) must have ≥44pt tap area or expand on touch (Apple HIG) +- `no-pie-overuse` - Avoid pie/donut for >5 categories; switch to bar chart for clarity +- `contrast-data` - Data lines/bars vs background ≥3:1; data text labels ≥4.5:1 (WCAG) +- `legend-interactive` - Legends should be clickable to toggle series visibility (MD) +- `direct-labeling` - For small datasets, label values directly on the chart to reduce eye travel +- `tooltip-keyboard` - Tooltip content must be keyboard-reachable and not rely on hover alone (WCAG) +- `sortable-table` - Data tables must support sorting with aria-sort indicating current sort state (WCAG) +- `axis-readability` - Axis ticks must not be cramped; maintain readable spacing, auto-skip on small screens +- `data-density` - Limit information density per chart to avoid cognitive overload; split into multiple charts if needed +- `trend-emphasis` - Emphasize data trends over decoration; avoid heavy gradients/shadows that obscure the data +- `gridline-subtle` - Grid lines should be low-contrast (e.g. gray-200) so they don't compete with data +- `focusable-elements` - Interactive chart elements (points, bars, slices) must be keyboard-navigable (WCAG) +- `screen-reader-summary` - Provide a text summary or aria-label describing the chart's key insight for screen readers (WCAG) +- `error-state-chart` - Data load failure must show error message with retry action, not a broken/empty chart +- `export-option` - For data-heavy products, offer CSV/image export of chart data +- `drill-down-consistency` - Drill-down interactions must maintain a clear back-path and hierarchy breadcrumb +- `time-scale-clarity` - Time series charts must clearly label time granularity (day/week/month) and allow switching + +## How to Use + +Search specific domains using the CLI tool below. + +--- + +## Prerequisites + +Check if Python is installed: + +```bash +python3 --version || python --version +``` + +If Python is not installed, install it based on user's OS: + +**macOS:** +```bash +brew install python3 +``` + +**Ubuntu/Debian:** +```bash +sudo apt update && sudo apt install python3 +``` + +**Windows:** +```powershell +winget install Python.Python.3.12 +``` + +--- + +## How to Use This Skill + +Use this skill when the user requests any of the following: + +| Scenario | Trigger Examples | Start From | +|----------|-----------------|------------| +| **New project / page** | "Build a landing page", "Build a dashboard" | Step 1 → Step 2 (design system) | +| **New component** | "Create a pricing card", "Add a modal" | Step 3 (domain search: style, ux) | +| **Choose style / color / font** | "What style fits a fintech app?", "Recommend a color palette" | Step 2 (design system) | +| **Review existing UI** | "Review this page for UX issues", "Check accessibility" | Quick Reference checklist above | +| **Fix a UI bug** | "Button hover is broken", "Layout shifts on load" | Quick Reference → relevant section | +| **Improve / optimize** | "Make this faster", "Improve mobile experience" | Step 3 (domain search: ux, react) | +| **Implement dark mode** | "Add dark mode support" | Step 3 (domain: style "dark mode") | +| **Add charts / data viz** | "Add an analytics dashboard chart" | Step 3 (domain: chart) | +| **Stack best practices** | "React performance tips"、"SwiftUI navigation" | Step 4 (stack search) | + +Follow this workflow: + +### Step 1: Analyze User Requirements + +Extract key information from user request: +- **Product type**: Entertainment (social, video, music, gaming), Tool (scanner, editor, converter), Productivity (task manager, notes, calendar), or hybrid +- **Target audience**: C-end consumer users; consider age group, usage context (commute, leisure, work) +- **Style keywords**: playful, vibrant, minimal, dark mode, content-first, immersive, etc. +- **Stack**: React Native (this project's only tech stack) + +### Step 2: Generate Design System (REQUIRED) + +**Always start with `--design-system`** to get comprehensive recommendations with reasoning: + +```bash +python3 ~/.hermes/skills/website-creator/ui-ux-pro-max/scripts/search.py " " --design-system [-p "Project Name"] +``` + +This command: +1. Searches domains in parallel (product, style, color, landing, typography) +2. Applies reasoning rules from `ui-reasoning.csv` to select best matches +3. Returns complete design system: pattern, style, colors, typography, effects +4. Includes anti-patterns to avoid + +**Example:** +```bash +python3 ~/.hermes/skills/website-creator/ui-ux-pro-max/scripts/search.py "beauty spa wellness service" --design-system -p "Serenity Spa" +``` + +### Step 2b: Persist Design System (Master + Overrides Pattern) + +To save the design system for **hierarchical retrieval across sessions**, add `--persist`: + +```bash +python3 ~/.hermes/skills/website-creator/ui-ux-pro-max/scripts/search.py "" --design-system --persist -p "Project Name" +``` + +This creates: +- `design-system/MASTER.md` — Global Source of Truth with all design rules +- `design-system/pages/` — Folder for page-specific overrides + +**With page-specific override:** +```bash +python3 ~/.hermes/skills/website-creator/ui-ux-pro-max/scripts/search.py "" --design-system --persist -p "Project Name" --page "dashboard" +``` + +This also creates: +- `design-system/pages/dashboard.md` — Page-specific deviations from Master + +**How hierarchical retrieval works:** +1. When building a specific page (e.g., "Checkout"), first check `design-system/pages/checkout.md` +2. If the page file exists, its rules **override** the Master file +3. If not, use `design-system/MASTER.md` exclusively + +**Context-aware retrieval prompt:** +``` +I am building the [Page Name] page. Please read design-system/MASTER.md. +Also check if design-system/pages/[page-name].md exists. +If the page file exists, prioritize its rules. +If not, use the Master rules exclusively. +Now, generate the code... +``` + +### Step 3: Supplement with Detailed Searches (as needed) + +After getting the design system, use domain searches to get additional details: + +```bash +python3 ~/.hermes/skills/website-creator/ui-ux-pro-max/scripts/search.py "" --domain [-n ] +``` + +**When to use detailed searches:** + +| Need | Domain | Example | +|------|--------|---------| +| Product type patterns | `product` | `--domain product "entertainment social"` | +| More style options | `style` | `--domain style "glassmorphism dark"` | +| Color palettes | `color` | `--domain color "entertainment vibrant"` | +| Font pairings | `typography` | `--domain typography "playful modern"` | +| Chart recommendations | `chart` | `--domain chart "real-time dashboard"` | +| UX best practices | `ux` | `--domain ux "animation accessibility"` | +| Alternative fonts | `typography` | `--domain typography "elegant luxury"` | +| Individual Google Fonts | `google-fonts` | `--domain google-fonts "sans serif popular variable"` | +| Landing structure | `landing` | `--domain landing "hero social-proof"` | +| React Native perf | `react` | `--domain react "rerender memo list"` | +| App interface a11y | `web` | `--domain web "accessibilityLabel touch safe-areas"` | +| AI prompt / CSS keywords | `prompt` | `--domain prompt "minimalism"` | + +### Step 4: Stack Guidelines (React Native) + +Get React Native implementation-specific best practices: + +```bash +python3 ~/.hermes/skills/website-creator/ui-ux-pro-max/scripts/search.py "" --stack react-native +``` + +--- + +## Search Reference + +### Available Domains + +| Domain | Use For | Example Keywords | +|--------|---------|------------------| +| `product` | Product type recommendations | SaaS, e-commerce, portfolio, healthcare, beauty, service | +| `style` | UI styles, colors, effects | glassmorphism, minimalism, dark mode, brutalism | +| `typography` | Font pairings, Google Fonts | elegant, playful, professional, modern | +| `color` | Color palettes by product type | saas, ecommerce, healthcare, beauty, fintech, service | +| `landing` | Page structure, CTA strategies | hero, hero-centric, testimonial, pricing, social-proof | +| `chart` | Chart types, library recommendations | trend, comparison, timeline, funnel, pie | +| `ux` | Best practices, anti-patterns | animation, accessibility, z-index, loading | +| `google-fonts` | Individual Google Fonts lookup | sans serif, monospace, japanese, variable font, popular | +| `react` | React/Next.js performance | waterfall, bundle, suspense, memo, rerender, cache | +| `web` | App interface guidelines (iOS/Android/React Native) | accessibilityLabel, touch targets, safe areas, Dynamic Type | +| `prompt` | AI prompts, CSS keywords | (style name) | + +### Available Stacks + +| Stack | Focus | +|-------|-------| +| `react-native` | Components, Navigation, Lists | + +--- + +## Example Workflow + +**User request:** "Make an AI search homepage." + +### Step 1: Analyze Requirements +- Product type: Tool (AI search engine) +- Target audience: C-end users looking for fast, intelligent search +- Style keywords: modern, minimal, content-first, dark mode +- Stack: React Native + +### Step 2: Generate Design System (REQUIRED) + +```bash +python3 ~/.hermes/skills/website-creator/ui-ux-pro-max/scripts/search.py "AI search tool modern minimal" --design-system -p "AI Search" +``` + +**Output:** Complete design system with pattern, style, colors, typography, effects, and anti-patterns. + +### Step 3: Supplement with Detailed Searches (as needed) + +```bash +# Get style options for a modern tool product +python3 ~/.hermes/skills/website-creator/ui-ux-pro-max/scripts/search.py "minimalism dark mode" --domain style + +# Get UX best practices for search interaction and loading +python3 ~/.hermes/skills/website-creator/ui-ux-pro-max/scripts/search.py "search loading animation" --domain ux +``` + +### Step 4: Stack Guidelines + +```bash +python3 ~/.hermes/skills/website-creator/ui-ux-pro-max/scripts/search.py "list performance navigation" --stack react-native +``` + +**Then:** Synthesize design system + detailed searches and implement the design. + +--- + +## Output Formats + +The `--design-system` flag supports two output formats: + +```bash +# ASCII box (default) - best for terminal display +python3 ~/.hermes/skills/website-creator/ui-ux-pro-max/scripts/search.py "fintech crypto" --design-system + +# Markdown - best for documentation +python3 ~/.hermes/skills/website-creator/ui-ux-pro-max/scripts/search.py "fintech crypto" --design-system -f markdown +``` + +--- + +## Tips for Better Results + +### Query Strategy + +- Use **multi-dimensional keywords** — combine product + industry + tone + density: `"entertainment social vibrant content-dense"` not just `"app"` +- Try different keywords for the same need: `"playful neon"` → `"vibrant dark"` → `"content-first minimal"` +- Use `--design-system` first for full recommendations, then `--domain` to deep-dive any dimension you're unsure about +- Always add `--stack react-native` for implementation-specific guidance + +### Common Sticking Points + +| Problem | What to Do | +|---------|------------| +| Can't decide on style/color | Re-run `--design-system` with different keywords | +| Dark mode contrast issues | Quick Reference §6: `color-dark-mode` + `color-accessible-pairs` | +| Animations feel unnatural | Quick Reference §7: `spring-physics` + `easing` + `exit-faster-than-enter` | +| Form UX is poor | Quick Reference §8: `inline-validation` + `error-clarity` + `focus-management` | +| Navigation feels confusing | Quick Reference §9: `nav-hierarchy` + `bottom-nav-limit` + `back-behavior` | +| Layout breaks on small screens | Quick Reference §5: `mobile-first` + `breakpoint-consistency` | +| Performance / jank | Quick Reference §3: `virtualize-lists` + `main-thread-budget` + `debounce-throttle` | + +### Pre-Delivery Checklist + +- Run `--domain ux "animation accessibility z-index loading"` as a UX validation pass before implementation +- Run through Quick Reference **§1–§3** (CRITICAL + HIGH) as a final review +- Test on 375px (small phone) and landscape orientation +- Verify behavior with **reduced-motion** enabled and **Dynamic Type** at largest size +- Check dark mode contrast independently (don't assume light mode values work) +- Confirm all touch targets ≥44pt and no content hidden behind safe areas + +--- + +## Common Rules for Professional UI + +These are frequently overlooked issues that make UI look unprofessional: +Scope notice: The rules below are for App UI (iOS/Android/React Native/Flutter), not desktop-web interaction patterns. + +### Icons & Visual Elements + +| Rule | Standard | Avoid | Why It Matters | +|------|----------|--------|----------------| +| **No Emoji as Structural Icons** | Use vector-based icons (e.g., Lucide, react-native-vector-icons, @expo/vector-icons). | Using emojis (🎨 🚀 ⚙️) for navigation, settings, or system controls. | Emojis are font-dependent, inconsistent across platforms, and cannot be controlled via design tokens. | +| **Vector-Only Assets** | Use SVG or platform vector icons that scale cleanly and support theming. | Raster PNG icons that blur or pixelate. | Ensures scalability, crisp rendering, and dark/light mode adaptability. | +| **Stable Interaction States** | Use color, opacity, or elevation transitions for press states without changing layout bounds. | Layout-shifting transforms that move surrounding content or trigger visual jitter. | Prevents unstable interactions and preserves smooth motion/perceived quality on mobile. | +| **Correct Brand Logos** | Use official brand assets and follow their usage guidelines (spacing, color, clear space). | Guessing logo paths, recoloring unofficially, or modifying proportions. | Prevents brand misuse and ensures legal/platform compliance. | +| **Consistent Icon Sizing** | Define icon sizes as design tokens (e.g., icon-sm, icon-md = 24pt, icon-lg). | Mixing arbitrary values like 20pt / 24pt / 28pt randomly. | Maintains rhythm and visual hierarchy across the interface. | +| **Stroke Consistency** | Use a consistent stroke width within the same visual layer (e.g., 1.5px or 2px). | Mixing thick and thin stroke styles arbitrarily. | Inconsistent strokes reduce perceived polish and cohesion. | +| **Filled vs Outline Discipline** | Use one icon style per hierarchy level. | Mixing filled and outline icons at the same hierarchy level. | Maintains semantic clarity and stylistic coherence. | +| **Touch Target Minimum** | Minimum 44×44pt interactive area (use hitSlop if icon is smaller). | Small icons without expanded tap area. | Meets accessibility and platform usability standards. | +| **Icon Alignment** | Align icons to text baseline and maintain consistent padding. | Misaligned icons or inconsistent spacing around them. | Prevents subtle visual imbalance that reduces perceived quality. | +| **Icon Contrast** | Follow WCAG contrast standards: 4.5:1 for small elements, 3:1 minimum for larger UI glyphs. | Low-contrast icons that blend into the background. | Ensures accessibility in both light and dark modes. | + + +### Interaction (App) + +| Rule | Do | Don't | +|------|----|----- | +| **Tap feedback** | Provide clear pressed feedback (ripple/opacity/elevation) within 80-150ms | No visual response on tap | +| **Animation timing** | Keep micro-interactions around 150-300ms with platform-native easing | Instant transitions or slow animations (>500ms) | +| **Accessibility focus** | Ensure screen reader focus order matches visual order and labels are descriptive | Unlabeled controls or confusing focus traversal | +| **Disabled state clarity** | Use disabled semantics (`disabled`/native disabled props), reduced emphasis, and no tap action | Controls that look tappable but do nothing | +| **Touch target minimum** | Keep tap areas >=44x44pt (iOS) or >=48x48dp (Android), expand hit area when icon is smaller | Tiny tap targets or icon-only hit areas without padding | +| **Gesture conflict prevention** | Keep one primary gesture per region and avoid nested tap/drag conflicts | Overlapping gestures causing accidental actions | +| **Semantic native controls** | Prefer native interactive primitives (`Button`, `Pressable`, platform equivalents) with proper accessibility roles | Generic containers used as primary controls without semantics | + +### Light/Dark Mode Contrast + +| Rule | Do | Don't | +|------|----|----- | +| **Surface readability (light)** | Keep cards/surfaces clearly separated from background with sufficient opacity/elevation | Overly transparent surfaces that blur hierarchy | +| **Text contrast (light)** | Maintain body text contrast >=4.5:1 against light surfaces | Low-contrast gray body text | +| **Text contrast (dark)** | Maintain primary text contrast >=4.5:1 and secondary text >=3:1 on dark surfaces | Dark mode text that blends into background | +| **Border and divider visibility** | Ensure separators are visible in both themes (not just light mode) | Theme-specific borders disappearing in one mode | +| **State contrast parity** | Keep pressed/focused/disabled states equally distinguishable in light and dark themes | Defining interaction states for one theme only | +| **Token-driven theming** | Use semantic color tokens mapped per theme across app surfaces/text/icons | Hardcoded per-screen hex values | +| **Scrim and modal legibility** | Use a modal scrim strong enough to isolate foreground content (typically 40-60% black) | Weak scrim that leaves background visually competing | + +### Layout & Spacing + +| Rule | Do | Don't | +|------|----|----- | +| **Safe-area compliance** | Respect top/bottom safe areas for all fixed headers, tab bars, and CTA bars | Placing fixed UI under notch, status bar, or gesture area | +| **System bar clearance** | Add spacing for status/navigation bars and gesture home indicator | Let tappable content collide with OS chrome | +| **Consistent content width** | Keep predictable content width per device class (phone/tablet) | Mixing arbitrary widths between screens | +| **8dp spacing rhythm** | Use a consistent 4/8dp spacing system for padding/gaps/section spacing | Random spacing increments with no rhythm | +| **Readable text measure** | Keep long-form text readable on large devices (avoid edge-to-edge paragraphs on tablets) | Full-width long text that hurts readability | +| **Section spacing hierarchy** | Define clear vertical rhythm tiers (e.g., 16/24/32/48) by hierarchy | Similar UI levels with inconsistent spacing | +| **Adaptive gutters by breakpoint** | Increase horizontal insets on larger widths and in landscape | Same narrow gutter on all device sizes/orientations | +| **Scroll and fixed element coexistence** | Add bottom/top content insets so lists are not hidden behind fixed bars | Scroll content obscured by sticky headers/footers | + +--- + +## Pre-Delivery Checklist + +Before delivering UI code, verify these items: +Scope notice: This checklist is for App UI (iOS/Android/React Native/Flutter). + +### Visual Quality +- [ ] No emojis used as icons (use SVG instead) +- [ ] All icons come from a consistent icon family and style +- [ ] Official brand assets are used with correct proportions and clear space +- [ ] Pressed-state visuals do not shift layout bounds or cause jitter +- [ ] Semantic theme tokens are used consistently (no ad-hoc per-screen hardcoded colors) + +### Interaction +- [ ] All tappable elements provide clear pressed feedback (ripple/opacity/elevation) +- [ ] Touch targets meet minimum size (>=44x44pt iOS, >=48x48dp Android) +- [ ] Micro-interaction timing stays in the 150-300ms range with native-feeling easing +- [ ] Disabled states are visually clear and non-interactive +- [ ] Screen reader focus order matches visual order, and interactive labels are descriptive +- [ ] Gesture regions avoid nested/conflicting interactions (tap/drag/back-swipe conflicts) + +### Light/Dark Mode +- [ ] Primary text contrast >=4.5:1 in both light and dark mode +- [ ] Secondary text contrast >=3:1 in both light and dark mode +- [ ] Dividers/borders and interaction states are distinguishable in both modes +- [ ] Modal/drawer scrim opacity is strong enough to preserve foreground legibility (typically 40-60% black) +- [ ] Both themes are tested before delivery (not inferred from a single theme) + +### Layout +- [ ] Safe areas are respected for headers, tab bars, and bottom CTA bars +- [ ] Scroll content is not hidden behind fixed/sticky bars +- [ ] Verified on small phone, large phone, and tablet (portrait + landscape) +- [ ] Horizontal insets/gutters adapt correctly by device size and orientation +- [ ] 4/8dp spacing rhythm is maintained across component, section, and page levels +- [ ] Long-form text measure remains readable on larger devices (no edge-to-edge paragraphs) + +### Accessibility +- [ ] All meaningful images/icons have accessibility labels +- [ ] Form fields have labels, hints, and clear error messages +- [ ] Color is not the only indicator +- [ ] Reduced motion and dynamic text size are supported without layout breakage +- [ ] Accessibility traits/roles/states (selected, disabled, expanded) are announced correctly \ No newline at end of file diff --git a/skills/website-creator/ui-ux-pro-max/data b/skills/website-creator/ui-ux-pro-max/data new file mode 120000 index 0000000..e5b9469 --- /dev/null +++ b/skills/website-creator/ui-ux-pro-max/data @@ -0,0 +1 @@ +../../../src/ui-ux-pro-max/data \ No newline at end of file diff --git a/skills/website-creator/ui-ux-pro-max/scripts b/skills/website-creator/ui-ux-pro-max/scripts new file mode 120000 index 0000000..ccb93f7 --- /dev/null +++ b/skills/website-creator/ui-ux-pro-max/scripts @@ -0,0 +1 @@ +../../../src/ui-ux-pro-max/scripts \ No newline at end of file

I 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 z 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 z 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 k5lvpYGA{VWmN0+6{MfZk_a4zeza3%1$=&r3q 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