From c2aba25814737d9ab055068ddad0ce2267a51815 Mon Sep 17 00:00:00 2001 From: bilalgursen Date: Sun, 2 Aug 2026 21:37:25 +0300 Subject: [PATCH] Update dependencies, enhance AI integration, and improve email content - Added @google/genai dependency for enhanced AI capabilities. - Updated CI workflow to include new environment variables for AI integration. - Refactored AI client to support multiple providers and improved key management. - Enhanced email templates for better user engagement and clarity. - Updated error handling in AI-related functions to provide clearer feedback. - Made various improvements to the application structure and code clarity. --- .gitea/workflows/ci.yaml | 2 + data/app.db | Bin 155648 -> 155648 bytes package.json | 1 + pnpm-lock.yaml | 286 +++++++++++++++++++++++++++++++++++ public/kolay-tercih-mark.png | Bin 0 -> 5129 bytes src/app/api/soru/route.ts | 52 ++----- src/app/sonuc/actions.ts | 2 +- src/lib/ai/cagri.ts | 158 +++++++++++++++++++ src/lib/ai/client.ts | 52 ++++++- src/lib/ai/rapor.ts | 35 ++--- src/lib/eposta.ts | 53 +++---- src/lib/sosyal-kanit.ts | 5 + 12 files changed, 548 insertions(+), 98 deletions(-) create mode 100644 public/kolay-tercih-mark.png create mode 100644 src/lib/ai/cagri.ts diff --git a/.gitea/workflows/ci.yaml b/.gitea/workflows/ci.yaml index aa8915c..125fd62 100644 --- a/.gitea/workflows/ci.yaml +++ b/.gitea/workflows/ci.yaml @@ -26,7 +26,9 @@ jobs: IYZICO_API_KEY=${{ secrets.IYZICO_API_KEY }} IYZICO_SECRET_KEY=${{ secrets.IYZICO_SECRET_KEY }} IYZICO_BASE_URL=${{ secrets.IYZICO_BASE_URL }} + AI_SAGLAYICI=${{ secrets.AI_SAGLAYICI }} ANTHROPIC_API_KEY=${{ secrets.ANTHROPIC_API_KEY }} + GEMINI_API_KEY=${{ secrets.GEMINI_API_KEY }} EOF - name: Build & deploy diff --git a/data/app.db b/data/app.db index 5cfc41e2430f17a9daf15faf2909d21326ff5da3..54927f32255bad8c931148b8323a3e967184d608 100644 GIT binary patch delta 472 zcmZoTz}awsbAmLZ`a~ILM)k&otqF`v@`c!VwHWx@`T6*!^2zWn=GEfZyjf6TF30xG z6^!L9p5<)pzP_O>EG*suQ7I8VNlt~KrrAM>?mqdMnMonRg_#B}L9WGV#^!;UNnUB? zuI`CRp;b9qWuDH+o*)HA9D_uFRW_X&b^#r(kb=~d z@`7;Zl=46?pWLL%(5$QgudF1`NYCJ+^!z|)AF#n(JxN*l-DV)XeQF(JKBEvDzcK^= zBz_NmW&T|L?fiH6CjrCj1>f|4eT=GkD$Kr&ghGl9q==BOS-~a|@WJ+p{fzk+07cTA AGynhq delta 416 zcmZoTz}awsbAmLZ%0wAwMwP~dtqF`v@`afBrZVui^Yigd<=6~?n*|l-a%|sR!C1~B zt5jO<(!^W;+Z-9LOr8uSp2IyS@AD7fN+yD6Nj&Fb2bYL zOJ#^pltp=-yJJz2fqSs8x0#uTzkxxOOTJHKQlME;a$cTav5!}{V|a3MuB)X*Wm!SF zQ-EWMOHOFKiI2Bm-t>PJjLP-#&ZU;&0a29|o~5Pv?grtR?uKEm=H8jUPT{%!CgEQ0 z70ymM1@VUY zm4%t*#zq$5Cc2J}-d=20.0.0'} + peerDependencies: + '@modelcontextprotocol/sdk': ^1.25.2 + peerDependenciesMeta: + '@modelcontextprotocol/sdk': + optional: true + '@hono/node-server@1.19.14': resolution: {integrity: sha512-GwtvgtXxnWsucXvbQXkRgqksiH2Qed37H9xHZocE5sA3N8O8O8/8FA3uclQXxXVzc9XBZuEOMK7+r02FmSpHtw==} engines: {node: '>=18.14.1'} @@ -1291,6 +1303,33 @@ packages: '@postman/tunnel-agent@0.6.8': resolution: {integrity: sha512-2U42SmZW5G+suEcS++zB94sBWNO4qD4bvETGFRFDTqSpYl5ksfjcPqzYpgQgXgUmb6dfz+fAGbkcRamounGm0w==} + '@protobufjs/aspromise@1.1.2': + resolution: {integrity: sha512-j+gKExEuLmKwvz3OgROXtrJ2UG2x8Ch2YZUxahh+s1F2HZ+wAceUNLkvy6zKCPVRkU++ZWQrdxsUeQXmcg4uoQ==} + + '@protobufjs/base64@1.1.2': + resolution: {integrity: sha512-AZkcAA5vnN/v4PDqKyMR5lx7hZttPDgClv83E//FMNhR2TMcLUhfRUBHCmSl0oi9zMgDDqRUJkSxO3wm85+XLg==} + + '@protobufjs/codegen@2.0.5': + resolution: {integrity: sha512-zgXFLzW3Ap33e6d0Wlj4MGIm6Ce8O89n/apUaGNB/jx+hw+ruWEp7EwGUshdLKVRCxZW12fp9r40E1mQrf/34g==} + + '@protobufjs/eventemitter@1.1.1': + resolution: {integrity: sha512-vW1GmwMZNnL+gMRaovlh9yZX74kc+TTU3FObkkurpMaRtBfLP3ldjS9KQWlwZgraRE0+dheEEoAxdzcJQ8eXZg==} + + '@protobufjs/fetch@1.1.1': + resolution: {integrity: sha512-GpptLrs57adMSuHi3VNj0mAF8dwh36LMaYF6XyJ6JMWlVsc+t42tm1HSEDmOs3A8fC9yyeisgLhsTVQokOZ0zw==} + + '@protobufjs/float@1.0.2': + resolution: {integrity: sha512-Ddb+kVXlXst9d+R9PfTIxh1EdNkgoRe5tOX6t01f1lYWOvJnSPDBlG241QLzcyPdoNTsblLUdujGSE4RzrTZGQ==} + + '@protobufjs/path@1.1.2': + resolution: {integrity: sha512-6JOcJ5Tm08dOHAbdR3GrvP+yUUfkjG5ePsHYczMFLq3ZmMkAD98cDgcT2iA1lJ9NVwFd4tH/iSSoe44YWkltEA==} + + '@protobufjs/pool@1.1.0': + resolution: {integrity: sha512-0kELaGSIDBKvcgS4zkjz1PeddatrjYcmMWOlAuAPwAeccUrPHdUqo/J6LiymHHEiJT5NrF1UVwxY14f+fy4WQw==} + + '@protobufjs/utf8@1.1.2': + resolution: {integrity: sha512-b1UQwcEZ4yCnMCD8DAL1VlbvBJE9/IX4FTIp7BG1xYpf29SLazLSrqUkj4w7Y5y7cCVP6E5tcqqcI0xemPkHug==} + '@radix-ui/number@1.1.2': resolution: {integrity: sha512-ceTwaxc4I5IOi97DgCotl3pqiyRGvffcc0oOsE2dQYaJOFIDsDt4VWG6xEbg1QePv9QWausCEIppud/tJ1wNig==} @@ -2305,6 +2344,9 @@ packages: '@types/react@19.2.17': resolution: {integrity: sha512-MXfmqaVPEVgkBT/aY0aGCkRWWtByiYQXo3xdQ8r5RzuFrPiRn8Gar2tQdXSUQ2GKV3bkXckek89V8wQBY2Q/Aw==} + '@types/retry@0.12.0': + resolution: {integrity: sha512-wWKOClTTiizcZhXnPY4wikVAwmdYHp8q6DmC+EJUzAMsycb7HB32Kh9RN4+0gExjmPmZSAQjgURXIGATPegAvA==} + '@types/trusted-types@2.0.7': resolution: {integrity: sha512-ScaPdn1dQczgbl0QFTeTOmVHFULt394XJgOQNoyVhZ6r2vLnMLJfBPd53SB52T/3G36VI1/g2MZaX0cwDuXsfw==} @@ -2742,6 +2784,9 @@ packages: bidi-js@1.0.3: resolution: {integrity: sha512-RKshQI1R3YQ+n9YJz2QQ147P66ELpa1FQEg20Dk8oW9t2KgLbpDLLp9aGZ7y8WHSshDknG0bknqGw5/tyCs5tw==} + bignumber.js@9.3.1: + resolution: {integrity: sha512-Ko0uX15oIUS7wJ3Rb30Fs6SkVbLmPBAKdlm7q9+ak9bbIeFf0MwuBsQV6z7+X768/cHsfg+WlysDWJcmthjsjQ==} + bindings@1.5.0: resolution: {integrity: sha512-p2q/t/mhvuOj/UeLlV6566GD/guowlr0hHxClI0W9m7MWYkL1F0hLo+0Aexs9HSPCtR1SXQ0TD3MMKrXZajbiQ==} @@ -2777,6 +2822,9 @@ packages: engines: {node: ^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7} hasBin: true + buffer-equal-constant-time@1.0.1: + resolution: {integrity: sha512-zRpUiDwd/xk6ADqPMATG8vc9VPrkck7T07OIx0gnjmJAnHnTVXNQG3vfvWNuiZIkwu9KrKdA1iJKfsfTVxE6NA==} + buffer-from@1.1.2: resolution: {integrity: sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==} @@ -3129,6 +3177,10 @@ packages: resolution: {integrity: sha512-jRFi8UDGo6j+odZiEpjazZaWqEal3w/basFjQHQEwVtZJGDpxbH1MeYluwCS8Xq5wmLJooDlMgvVarmWfGM44g==} engines: {node: '>=0.10'} + data-uri-to-buffer@4.0.1: + resolution: {integrity: sha512-0R9ikRb668HB7QDxT1vkpuUBtqc53YyAwMwGeUFKRojY/NWKvdZ+9UYtRfGmhqNbRkTSVpMbmyhXipFFv2cb/A==} + engines: {node: '>= 12'} + data-view-buffer@1.0.2: resolution: {integrity: sha512-EmKO5V3OLXh1rtK2wgXRansaK1/mtVdTUEiEI0W8RkvgT05kfxaH29PliLnpLP73yYO6142Q72QNa8Wx/A5CqQ==} engines: {node: '>= 0.4'} @@ -3372,6 +3424,9 @@ packages: ecc-jsbn@0.1.2: resolution: {integrity: sha512-eh9O+hwRHNbG4BLTjEl3nw044CkGm5X6LoaCf7LPp7UU8Qrt47JYNi6nPX8xjW97TKGKm1ouctg0QSpZe9qrnw==} + ecdsa-sig-formatter@1.0.11: + resolution: {integrity: sha512-nagl3RYrbNv6kQkeJIpt6NJZy8twLB/2vtz6yN9Z4vRKHN4/QZJIEbqohALSgwKdnksuY3k5Addp5lg8sVoVcQ==} + ee-first@1.1.1: resolution: {integrity: sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==} @@ -3689,6 +3744,10 @@ packages: picomatch: optional: true + fetch-blob@3.2.0: + resolution: {integrity: sha512-7yAQpD2UMJzLi1Dqv7qFYnPbaPx7ZfFK6PiIxQ4PfkGPyNyl2Ugx+a/umUonmKqjhM4DnfbMvdX6otXq83soQQ==} + engines: {node: ^12.20 || >= 14.13} + fflate@0.8.3: resolution: {integrity: sha512-tbZNuJrLwGUp3zshBtdy4W+ORxZuIh8a5ilyIEQDC5rY1f3U20JMry0Ll3WBzU58EZKsEuJFXhb5gwv8CsPvgA==} @@ -3736,6 +3795,10 @@ packages: forever-agent@0.6.1: resolution: {integrity: sha512-j0KLYPhm6zeac4lz3oJ3o65qvgQCcPubiyotZrXqEaG4hNagNYO8qdlUrX5vwqv9ohqeT/Z3j6+yW067yWWdUw==} + formdata-polyfill@4.0.10: + resolution: {integrity: sha512-buewHzMvYL29jdeQTVILecSaZKnt/RJWjoZCF5OW60Z67/GmSLBkOFM7qh1PI3zFNtJbaZL5eQu1vLfazOwj4g==} + engines: {node: '>=12.20.0'} + forwarded@0.2.0: resolution: {integrity: sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow==} engines: {node: '>= 0.6'} @@ -3783,6 +3846,14 @@ packages: fuzzysort@3.1.0: resolution: {integrity: sha512-sR9BNCjBg6LNgwvxlBd0sBABvQitkLzoVY9MYYROQVX/FvfJ4Mai9LsGhDgd8qYdds0bY77VzYd5iuB+v5rwQQ==} + gaxios@7.3.0: + resolution: {integrity: sha512-RB5vLV+vvQeoFPCX4QMK6/hjVkbIamPp1QSUD0CiZcnj12qbpiL+pLbYtgD+oZkWl0tl9z+o2Utp+MpM3QRhBA==} + engines: {node: '>=18'} + + gcp-metadata@8.1.2: + resolution: {integrity: sha512-zV/5HKTfCeKWnxG0Dmrw51hEWFGfcF2xiXqcA3+J90WDuP0SvoiSO5ORvcBsifmx/FoIjgQN3oNOGaQ5PhLFkg==} + engines: {node: '>=18'} + generator-function@2.0.1: resolution: {integrity: sha512-SFdFmIJi+ybC0vjlHN0ZGVGHc3lgE0DxPAT0djjVg+kjOnSqclqmj0KQ7ykTOLP6YxoqOvuAODGdcHJn+43q3g==} engines: {node: '>= 0.4'} @@ -3852,6 +3923,14 @@ packages: resolution: {integrity: sha512-DpLKbNU4WylpxJykQujfCcwYWiV/Jhm50Goo0wrVILAv5jOr9d+H+UR3PhSCD2rCCEIg0uc+G+muBTwD54JhDQ==} engines: {node: '>= 0.4'} + google-auth-library@10.9.1: + resolution: {integrity: sha512-i1ydyHrqcIxXkWh/uBmVkzCvIuq5yiK2ATndIe5XxKholrG/MTYP9xGYka4sQhrbIAgGjL2B6NOE7rFaiF3fXw==} + engines: {node: '>=18'} + + google-logging-utils@1.1.3: + resolution: {integrity: sha512-eAmLkjDjAFCVXg7A1unxHsLf961m6y17QFqXqAXGj/gVkKFrEICfStRfwUlGNfeCEjNRa32JEWOUTlYXPyyKvA==} + engines: {node: '>=14'} + gopd@1.2.0: resolution: {integrity: sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==} engines: {node: '>= 0.4'} @@ -3928,6 +4007,10 @@ packages: resolution: {integrity: sha512-G5akfn7eKbpDN+8nPS/cb57YeA1jLTVxjpCj7tmm3QKPdyDy7T+qSC40e9ptydSWvkwjSXw1VbkpyEm39ukeAg==} engines: {node: '>=0.10'} + https-proxy-agent@7.0.6: + resolution: {integrity: sha512-vK9P5/iUfdl95AI+JVyUuIcVtd4ofvtrOr3HNtM2yxC9bnMbEdp3x01OhQNnjb8IJYi38VlTE3mBXwcfvywuSw==} + engines: {node: '>= 14'} + human-signals@2.1.0: resolution: {integrity: sha512-B4FFZ6q/T2jhhksgkbEW3HBvWIfDW85snkQgawt07S7J5QXTk6BkNV+0yAeZrM5QpMAdYlocGoljn0sJ/WQkFw==} engines: {node: '>=10.17.0'} @@ -4242,6 +4325,9 @@ packages: engines: {node: '>=6'} hasBin: true + json-bigint@1.0.0: + resolution: {integrity: sha512-SiPv/8VpZuWbvLSMtTDU8hEfrZWg/mH/nV/b4o0CYbSxu1UIQPLdwKOCIyLQX+VIPO5vrLX3i8qtqFyhdPSUSQ==} + json-buffer@3.0.1: resolution: {integrity: sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ==} @@ -4293,6 +4379,12 @@ packages: resolution: {integrity: sha512-ZZow9HBI5O6EPgSJLUb8n2NKgmVWTwCvHGwFuJlMjvLFqlGG6pjirPhtdsseaLZjSibD8eegzmYpUZwoIlj2cQ==} engines: {node: '>=4.0'} + jwa@2.0.1: + resolution: {integrity: sha512-hRF04fqJIP8Abbkq5NKGN0Bbr3JxlQ+qhZufXVr0DvujKy93ZCbXZMHDL4EOtodSbCWxOqR8MS1tXA5hwqCXDg==} + + jws@4.0.1: + resolution: {integrity: sha512-EKI/M/yqPncGUUh44xz0PxSidXFr/+r0pA70+gIYhjv+et7yxM+s29Y+VGDkovRofQem0fs7Uvf4+YmAdyRduA==} + katex@0.16.47: resolution: {integrity: sha512-Eeo8Ys1doU1z+x8AZsPpQu+p/QcZBI5PeOo7QGQdy2x2m0MU/hYagBbGOmXwr5KVbEfVuWv9LpnQWeehogurjg==} hasBin: true @@ -4444,6 +4536,9 @@ packages: resolution: {integrity: sha512-i24m8rpwhmPIS4zscNzK6MSEhk0DUWa/8iYQWxhffV8jkI4Phvs3F+quL5xvS0gdQR0FyTCMMH33Y78dDTzzIw==} engines: {node: '>=18'} + long@5.3.2: + resolution: {integrity: sha512-mNAgZ1GmyNhD7AuqnTG3/VQ26o760+ZYBPKjPvugO8+nLbYfX6TVpJPseBvopbdY+qpZ/lKUnmEc1LeZYS3QAA==} + longest-streak@3.1.0: resolution: {integrity: sha512-9Ri+o0JYgehTaVBBDoMqIl8GXtbWg711O3srftcHhZ0dqnETqLaoIK0x17fUw9rFSlK/0NlsKe0Ahhyl5pXE2g==} @@ -4759,10 +4854,19 @@ packages: resolution: {integrity: sha512-W5ZNO5KRPB5TkYmGVD9F6YqhsglXJzE6etpbmT+f6EQElhiX/UTG551cnsRGvLG3fyZEg9HwaDmNmj5nwJ4z9g==} engines: {node: '>=10'} + node-domexception@1.0.0: + resolution: {integrity: sha512-/jKZoMpw0F8GRwl4/eLROPA3cfcXtLApP0QzLmUT/HuPCZWyB7IY9ZrMeKw2O/nFIqPQB3PVM9aYm0F312AXDQ==} + engines: {node: '>=10.5.0'} + deprecated: Use your platform's native DOMException instead + node-exports-info@1.6.2: resolution: {integrity: sha512-kXs9Go0cah0qHVV2v389IXQLdLCeE1xfFtjOAF+iobu0OIoG1pje8At2vMHyaPMiPMnG/LWP50twML21eMcAag==} engines: {node: '>= 0.4'} + node-fetch@3.3.2: + resolution: {integrity: sha512-dRB78srN/l6gqWulah9SrxeYnxeddIG30+GOqK/9OlLVyLg3HPnr6SqOWTWOXKRwC2eGYCkZ59NNuSgvSrpgOA==} + engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0} + node-releases@2.0.51: resolution: {integrity: sha512-wRNIrw4DmVLKQlbgOMdkMx27Wrpzes2hh5Jtbi2bjPd+4wJstWIqP5A+lscnqbm0xxmT5Bpg8Lec5ItEBwx6BQ==} engines: {node: '>=18'} @@ -4877,6 +4981,10 @@ packages: resolution: {integrity: sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw==} engines: {node: '>=10'} + p-retry@4.6.2: + resolution: {integrity: sha512-312Id396EbJdvRONlngUx0NydfrIQ5lsYu0znKVUzVvArzEIt08V1qhtyESbGVd1FGX7UKtiFp5uwKZdM8wIuQ==} + engines: {node: '>=8'} + p-try@2.2.0: resolution: {integrity: sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ==} engines: {node: '>=6'} @@ -5025,6 +5133,10 @@ packages: property-information@7.2.0: resolution: {integrity: sha512-IAtzIB6sUiWaJYrX9smp3V46pBGbBeLFRGdh25kg1334VcBlD8HzhPeNIWQH9zhGmo2itIe25EHt9dQP7G5hmg==} + protobufjs@7.6.5: + resolution: {integrity: sha512-/FPD0nUc9jH6rfFjji9IBqOz4pcSE3CsT1m7Ep6Mdb0LxSUMj8hgl6GomOvZzpNpAqqGaXA0P3VSrZLFzIhQrw==} + engines: {node: '>=12.0.0'} + proxy-addr@2.0.7: resolution: {integrity: sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg==} engines: {node: '>= 0.10'} @@ -5213,6 +5325,10 @@ packages: restructure@3.0.2: resolution: {integrity: sha512-gSfoiOEA0VPE6Tukkrr7I0RBdE0s7H1eFCDBk05l1KIQT1UIKNc5JZy6jdyW6eYH3aR3g5b3PuL77rq0hvwtAw==} + retry@0.13.1: + resolution: {integrity: sha512-XQBQ3I8W1Cge0Seh+6gjj03LbmRFWuoszgK9ooCpwYIrhhoO80pfq4cUkU5DkknwfOfFteRwlZ56PYOGYyFWdg==} + engines: {node: '>= 4'} + reusify@1.1.0: resolution: {integrity: sha512-g6QUff04oZpHs0eG5p83rFLhHeV00ug/Yf9nZM6fLeUrPguBTkTQOdpAWWspMh55TZfVQDPaN3NQJfbVRAxdIw==} engines: {iojs: '>=1.0.0', node: '>=0.10.0'} @@ -5776,6 +5892,10 @@ packages: resolution: {integrity: sha512-t20zYkrSf868+j/p31cRIGN28Phrjm3nRSLR2fyc2tiWi4cZGVdv68yNlwnIINTkMTmPoMiSlc0OadaO7DXZaQ==} engines: {node: '>= 6'} + web-streams-polyfill@3.3.3: + resolution: {integrity: sha512-d2JWLCivmZYTSIoge9MsgFCZrt571BikcWGYkjC1khllbTeDlGqZ2D8vD8E/lJa8WGWbb7Plm8/XJYV7IJHZZw==} + engines: {node: '>= 8'} + which-boxed-primitive@1.1.1: resolution: {integrity: sha512-TbX3mj8n0odCBFVlY8AxkqcHASw3L60jIuF8jFP78az3C2YhmGvqbHBpAjTRH2/xqYunrJ9g1jSyjCjpoWzIAA==} engines: {node: '>= 0.4'} @@ -6465,6 +6585,19 @@ snapshots: '@floating-ui/utils@0.2.12': {} + '@google/genai@2.15.0(@modelcontextprotocol/sdk@1.29.0(zod@4.4.3))': + dependencies: + google-auth-library: 10.9.1 + p-retry: 4.6.2 + protobufjs: 7.6.5 + ws: 8.21.1 + optionalDependencies: + '@modelcontextprotocol/sdk': 1.29.0(zod@4.4.3) + transitivePeerDependencies: + - bufferutil + - supports-color + - utf-8-validate + '@hono/node-server@1.19.14(hono@4.12.31)': dependencies: hono: 4.12.31 @@ -6693,6 +6826,29 @@ snapshots: transitivePeerDependencies: - supports-color + '@modelcontextprotocol/sdk@1.29.0(zod@4.4.3)': + dependencies: + '@hono/node-server': 1.19.14(hono@4.12.31) + ajv: 8.20.0 + ajv-formats: 3.0.1(ajv@8.20.0) + content-type: 1.0.5 + cors: 2.8.6 + cross-spawn: 7.0.6 + eventsource: 3.0.7 + eventsource-parser: 3.1.0 + express: 5.2.1 + express-rate-limit: 8.6.0(express@5.2.1) + hono: 4.12.31 + jose: 6.2.3 + json-schema-typed: 8.0.2 + pkce-challenge: 5.0.1 + raw-body: 3.0.2 + zod: 4.4.3 + zod-to-json-schema: 3.25.2(zod@4.4.3) + transitivePeerDependencies: + - supports-color + optional: true + '@napi-rs/wasm-runtime@1.1.6(@emnapi/core@1.10.0)(@emnapi/runtime@1.10.0)': dependencies: '@emnapi/core': 1.10.0 @@ -6780,6 +6936,26 @@ snapshots: dependencies: safe-buffer: 5.2.1 + '@protobufjs/aspromise@1.1.2': {} + + '@protobufjs/base64@1.1.2': {} + + '@protobufjs/codegen@2.0.5': {} + + '@protobufjs/eventemitter@1.1.1': {} + + '@protobufjs/fetch@1.1.1': + dependencies: + '@protobufjs/aspromise': 1.1.2 + + '@protobufjs/float@1.0.2': {} + + '@protobufjs/path@1.1.2': {} + + '@protobufjs/pool@1.1.0': {} + + '@protobufjs/utf8@1.1.2': {} + '@radix-ui/number@1.1.2': {} '@radix-ui/primitive@1.1.6': {} @@ -7926,6 +8102,8 @@ snapshots: dependencies: csstype: 3.2.3 + '@types/retry@0.12.0': {} + '@types/trusted-types@2.0.7': optional: true @@ -8323,6 +8501,8 @@ snapshots: dependencies: require-from-string: 2.0.2 + bignumber.js@9.3.1: {} + bindings@1.5.0: dependencies: file-uri-to-path: 1.0.0 @@ -8378,6 +8558,8 @@ snapshots: node-releases: 2.0.51 update-browserslist-db: 1.2.3(browserslist@4.28.6) + buffer-equal-constant-time@1.0.1: {} + buffer-from@1.1.2: {} buffer@5.7.1: @@ -8728,6 +8910,8 @@ snapshots: dependencies: assert-plus: 1.0.0 + data-uri-to-buffer@4.0.1: {} + data-view-buffer@1.0.2: dependencies: call-bound: 1.0.4 @@ -8864,6 +9048,10 @@ snapshots: jsbn: 0.1.1 safer-buffer: 2.1.2 + ecdsa-sig-formatter@1.0.11: + dependencies: + safe-buffer: 5.2.1 + ee-first@1.1.1: {} electron-to-chromium@1.5.394: {} @@ -9429,6 +9617,11 @@ snapshots: optionalDependencies: picomatch: 4.0.5 + fetch-blob@3.2.0: + dependencies: + node-domexception: 1.0.0 + web-streams-polyfill: 3.3.3 + fflate@0.8.3: {} figures@6.1.0: @@ -9490,6 +9683,10 @@ snapshots: forever-agent@0.6.1: {} + formdata-polyfill@4.0.10: + dependencies: + fetch-blob: 3.2.0 + forwarded@0.2.0: {} framer-motion@12.42.2(react-dom@19.2.4(react@19.2.4))(react@19.2.4): @@ -9532,6 +9729,22 @@ snapshots: fuzzysort@3.1.0: {} + gaxios@7.3.0: + dependencies: + extend: 3.0.2 + https-proxy-agent: 7.0.6 + node-fetch: 3.3.2 + transitivePeerDependencies: + - supports-color + + gcp-metadata@8.1.2: + dependencies: + gaxios: 7.3.0 + google-logging-utils: 1.1.3 + json-bigint: 1.0.0 + transitivePeerDependencies: + - supports-color + generator-function@2.0.1: {} gensync@1.0.0-beta.2: {} @@ -9600,6 +9813,19 @@ snapshots: define-properties: 1.2.1 gopd: 1.2.0 + google-auth-library@10.9.1: + dependencies: + base64-js: 1.5.1 + ecdsa-sig-formatter: 1.0.11 + gaxios: 7.3.0 + gcp-metadata: 8.1.2 + google-logging-utils: 1.1.3 + jws: 4.0.1 + transitivePeerDependencies: + - supports-color + + google-logging-utils@1.1.3: {} + gopd@1.2.0: {} graceful-fs@4.2.11: {} @@ -9698,6 +9924,13 @@ snapshots: jsprim: 2.0.2 sshpk: 1.18.0 + https-proxy-agent@7.0.6: + dependencies: + agent-base: 7.1.4 + debug: 4.4.3 + transitivePeerDependencies: + - supports-color + human-signals@2.1.0: {} human-signals@8.0.1: {} @@ -9963,6 +10196,10 @@ snapshots: jsesc@3.1.0: {} + json-bigint@1.0.0: + dependencies: + bignumber.js: 9.3.1 + json-buffer@3.0.1: {} json-parse-even-better-errors@2.3.1: {} @@ -10012,6 +10249,17 @@ snapshots: object.assign: 4.1.7 object.values: 1.2.1 + jwa@2.0.1: + dependencies: + buffer-equal-constant-time: 1.0.1 + ecdsa-sig-formatter: 1.0.11 + safe-buffer: 5.2.1 + + jws@4.0.1: + dependencies: + jwa: 2.0.1 + safe-buffer: 5.2.1 + katex@0.16.47: dependencies: commander: 8.3.0 @@ -10136,6 +10384,8 @@ snapshots: chalk: 5.6.2 is-unicode-supported: 1.3.0 + long@5.3.2: {} + longest-streak@3.1.0: {} loose-envify@1.4.0: @@ -10643,6 +10893,8 @@ snapshots: dependencies: semver: 7.8.5 + node-domexception@1.0.0: {} + node-exports-info@1.6.2: dependencies: array.prototype.flatmap: 1.3.3 @@ -10650,6 +10902,12 @@ snapshots: object.entries: 1.1.9 semver: 6.3.1 + node-fetch@3.3.2: + dependencies: + data-uri-to-buffer: 4.0.1 + fetch-blob: 3.2.0 + formdata-polyfill: 4.0.10 + node-releases@2.0.51: {} normalize-svg-path@1.1.0: @@ -10797,6 +11055,11 @@ snapshots: dependencies: p-limit: 3.1.0 + p-retry@4.6.2: + dependencies: + '@types/retry': 0.12.0 + retry: 0.13.1 + p-try@2.2.0: {} package-manager-detector@1.8.0: {} @@ -10957,6 +11220,20 @@ snapshots: property-information@7.2.0: {} + protobufjs@7.6.5: + dependencies: + '@protobufjs/aspromise': 1.1.2 + '@protobufjs/base64': 1.1.2 + '@protobufjs/codegen': 2.0.5 + '@protobufjs/eventemitter': 1.1.1 + '@protobufjs/fetch': 1.1.1 + '@protobufjs/float': 1.0.2 + '@protobufjs/path': 1.1.2 + '@protobufjs/pool': 1.1.0 + '@protobufjs/utf8': 1.1.2 + '@types/node': 20.19.43 + long: 5.3.2 + proxy-addr@2.0.7: dependencies: forwarded: 0.2.0 @@ -11242,6 +11519,8 @@ snapshots: restructure@3.0.2: {} + retry@0.13.1: {} + reusify@1.1.0: {} robust-predicates@3.0.3: {} @@ -11977,6 +12256,8 @@ snapshots: string_decoder: 1.3.0 util-deprecate: 1.0.2 + web-streams-polyfill@3.3.3: {} + which-boxed-primitive@1.1.1: dependencies: is-bigint: 1.1.0 @@ -12053,6 +12334,11 @@ snapshots: dependencies: zod: 3.25.76 + zod-to-json-schema@3.25.2(zod@4.4.3): + dependencies: + zod: 4.4.3 + optional: true + zod-validation-error@4.0.2(zod@4.4.3): dependencies: zod: 4.4.3 diff --git a/public/kolay-tercih-mark.png b/public/kolay-tercih-mark.png new file mode 100644 index 0000000000000000000000000000000000000000..3ff57e2b2bbbb0be00609768c7dfc8b8e7eafacd GIT binary patch literal 5129 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 DxJ5_x literal 0 HcmV?d00001 diff --git a/src/app/api/soru/route.ts b/src/app/api/soru/route.ts index 2381ce2..68dcc3d 100644 --- a/src/app/api/soru/route.ts +++ b/src/app/api/soru/route.ts @@ -3,7 +3,8 @@ import type { NextRequest } from "next/server"; import { getSession } from "@/lib/session"; import { appDb, schema } from "@/lib/appdb"; import { spendCreditForMessage, grantCredits } from "@/lib/credits"; -import { getAnthropic, SOHBET_MODEL, SORUMLULUK_REDDI } from "@/lib/ai/client"; +import { anahtarVar, SORUMLULUK_REDDI } from "@/lib/ai/client"; +import { sohbetAkisi } from "@/lib/ai/cagri"; import { listeOzetiCikar, type RaporSonuc } from "@/lib/ai/rapor"; import type { RaporParams } from "@/lib/rapor-havuzu"; import { secimOzeti } from "@/lib/sihirbaz"; @@ -183,15 +184,11 @@ export async function POST(request: NextRequest) { }); // Dev'de API key yoksa danışman mock cevap stream'ler (prod'da asla) - if ( - process.env.NODE_ENV !== "production" && - !process.env.ANTHROPIC_API_KEY && - !process.env.ANTHROPIC_AUTH_TOKEN - ) { + if (process.env.NODE_ENV !== "production" && !anahtarVar()) { const mockEncoder = new TextEncoder(); const mockUserId = session.user.id; const cevap = - `(Dev mock — ANTHROPIC_API_KEY tanımlı değil.) Sorunu aldım: "${mesaj.slice(0, 120)}". ` + + `(Dev mock — Yapay Zeka anahtarı tanımlı değil.) Sorunu aldım: "${mesaj.slice(0, 120)}". ` + `Gerçek danışman burada listen ve seçimlerin bağlamında yanıt verecek. ` + `${SORUMLULUK_REDDI}`; const mockReadable = new ReadableStream({ @@ -219,17 +216,14 @@ export async function POST(request: NextRequest) { }); } - let client; - try { - client = getAnthropic(); - } catch { + if (!anahtarVar()) { await grantCredits({ userId: session.user.id, delta: 1, reason: "refund", refId: kullaniciMesajId, }); - console.error("[soru] Yapay Zeka istemcisi kurulamadı (anahtar eksik olabilir)"); + console.error("[soru] Yapay Zeka anahtarı tanımlı değil"); return Response.json( { error: @@ -250,36 +244,20 @@ export async function POST(request: NextRequest) { const encoder = new TextEncoder(); const userId = session.user.id; - let stream; - try { - stream = client.messages.stream({ - model: SOHBET_MODEL, - max_tokens: 1024, - system, - messages: mesajlar, - }); - } catch { - await grantCredits({ userId, delta: 1, reason: "refund", refId: kullaniciMesajId }); - return Response.json({ error: "Yapay Zeka'ya ulaşılamadı." }, { status: 502 }); - } - const readable = new ReadableStream({ async start(controller) { let tamamlandi = false; try { - for await (const event of stream) { - if ( - event.type === "content_block_delta" && - event.delta.type === "text_delta" - ) { - controller.enqueue(encoder.encode(event.delta.text)); - } + // Sağlayıcıya bağlanma hatası da ilk parçada burada yakalanır + let metin = ""; + for await (const parca of sohbetAkisi({ + system, + mesajlar, + maxTokens: 1024, + })) { + metin += parca; + controller.enqueue(encoder.encode(parca)); } - const final = await stream.finalMessage(); - const metin = final.content - .filter((b) => b.type === "text") - .map((b) => b.text) - .join(""); await appDb.insert(schema.chatMessages).values({ id: crypto.randomUUID(), userId, diff --git a/src/app/sonuc/actions.ts b/src/app/sonuc/actions.ts index ba741bf..b1c5afa 100644 --- a/src/app/sonuc/actions.ts +++ b/src/app/sonuc/actions.ts @@ -43,7 +43,7 @@ function aiHataMesaji(err: unknown): string { if (err instanceof RaporUretimHatasi) return err.message; if (err instanceof Error && err.message === "AI_KEY_MISSING") { // Yapılandırma detayı log'a; kullanıcıya altyapı sızdırmayan mesaj - console.error("[liste] ANTHROPIC_API_KEY eksik — üretim başlatılamadı"); + console.error("[liste] Yapay Zeka anahtarı eksik — üretim başlatılamadı"); return "Liste üretimi şu anda başlatılamıyor. Birazdan tekrar dener misin? Harcanan kredin otomatik iade edilir."; } console.error("[liste] üretim hatası:", err); diff --git a/src/lib/ai/cagri.ts b/src/lib/ai/cagri.ts new file mode 100644 index 0000000..381a424 --- /dev/null +++ b/src/lib/ai/cagri.ts @@ -0,0 +1,158 @@ +import { z } from "zod"; +import { zodOutputFormat } from "@anthropic-ai/sdk/helpers/zod"; +import { ThinkingLevel } from "@google/genai"; +import { + getAnthropic, + getGemini, + raporModeli, + saglayici, + sohbetModeli, +} from "./client"; + +/** + * Sağlayıcıdan bağımsız AI çağrı katmanı. Anthropic ve Gemini'nin SDK'ları + * yapılandırılmış çıktı ve stream için farklı şekiller kullanıyor; rapor ve + * sohbet kodu bu farkı görmesin diye tek arayüz burada toplanıyor. + */ + +/** + * Gemini JSON Schema'nın yalnızca bir alt kümesini destekliyor; zod'un ürettiği + * $schema ve additionalProperties alanları isteği reddettiriyor, ayıklanıyor. + */ +function geminiSemasi(sema: z.ZodType): unknown { + const temizle = (dugum: unknown): unknown => { + if (Array.isArray(dugum)) return dugum.map(temizle); + if (dugum && typeof dugum === "object") { + const cikti: Record = {}; + for (const [k, v] of Object.entries(dugum as Record)) { + if (k === "$schema" || k === "additionalProperties") continue; + cikti[k] = temizle(v); + } + return cikti; + } + return dugum; + }; + return temizle(z.toJSONSchema(sema)); +} + +/** + * Gemini ücretsiz katmanı yoğunlukta 503/429 döndürüyor; bu geçici hatalarda + * artan bekleme ile yeniden dene. Kalıcı hatalar (400, 403) olduğu gibi fırlar. + */ +const GECICI_KODLAR = new Set([429, 500, 502, 503, 504]); + +async function geciciHatadaTekrarla( + islem: () => Promise, + denemeSayisi = 3, +): Promise { + let sonHata: unknown; + for (let i = 0; i < denemeSayisi; i++) { + try { + return await islem(); + } catch (hata) { + const kod = (hata as { status?: number })?.status; + if (kod == null || !GECICI_KODLAR.has(kod) || i === denemeSayisi - 1) { + throw hata; + } + sonHata = hata; + await new Promise((r) => setTimeout(r, 2000 * 2 ** i)); + } + } + throw sonHata; +} + +/** + * Şemaya uyan tek seferlik çıktı üretir. Şemaya uymayan yanıtta null döner + * (çağıran taraf yeniden dener). + */ +export async function yapilandirilmisUret(opts: { + system: string; + kullanici: string; + sema: T; + maxTokens: number; +}): Promise | null> { + if (saglayici() === "gemini") { + const yanit = await geciciHatadaTekrarla(() => + getGemini().models.generateContent({ + model: raporModeli(), + contents: opts.kullanici, + config: { + systemInstruction: opts.system, + maxOutputTokens: opts.maxTokens, + responseMimeType: "application/json", + responseJsonSchema: geminiSemasi(opts.sema), + thinkingConfig: { thinkingLevel: ThinkingLevel.HIGH }, + }, + }), + ); + const metin = yanit.text; + if (!metin) return null; + try { + const parsed = opts.sema.safeParse(JSON.parse(metin)); + return parsed.success ? parsed.data : null; + } catch { + return null; + } + } + + const yanit = await getAnthropic().messages.parse({ + model: raporModeli(), + max_tokens: opts.maxTokens, + thinking: { type: "adaptive" }, + system: opts.system, + messages: [{ role: "user", content: opts.kullanici }], + output_config: { format: zodOutputFormat(opts.sema) }, + }); + return yanit.parsed_output ?? null; +} + +export type SohbetMesaji = { role: "user" | "assistant"; content: string }; + +/** + * Sohbet yanıtını parça parça üretir. Sağlayıcıya bakmaksızın düz metin + * parçaları yield eder; çağıran taraf hem stream'ler hem biriktirip kaydeder. + */ +export async function* sohbetAkisi(opts: { + system: string; + mesajlar: SohbetMesaji[]; + maxTokens: number; +}): AsyncGenerator { + if (saglayici() === "gemini") { + const akis = await geciciHatadaTekrarla(() => + getGemini().models.generateContentStream({ + model: sohbetModeli(), + contents: opts.mesajlar.map((m) => ({ + // Gemini "assistant" yerine "model" rolünü kullanır + role: m.role === "assistant" ? "model" : "user", + parts: [{ text: m.content }], + })), + config: { + systemInstruction: opts.system, + maxOutputTokens: opts.maxTokens, + // Sohbet kısa ve hızlı olmalı; uzun düşünme adayı bekletiyor + thinkingConfig: { thinkingLevel: ThinkingLevel.LOW }, + }, + }), + ); + for await (const parca of akis) { + const metin = parca.text; + if (metin) yield metin; + } + return; + } + + const akis = getAnthropic().messages.stream({ + model: sohbetModeli(), + max_tokens: opts.maxTokens, + system: opts.system, + messages: opts.mesajlar, + }); + for await (const olay of akis) { + if ( + olay.type === "content_block_delta" && + olay.delta.type === "text_delta" + ) { + yield olay.delta.text; + } + } +} diff --git a/src/lib/ai/client.ts b/src/lib/ai/client.ts index 05ed35d..c147661 100644 --- a/src/lib/ai/client.ts +++ b/src/lib/ai/client.ts @@ -1,11 +1,44 @@ import Anthropic from "@anthropic-ai/sdk"; +import { GoogleGenAI } from "@google/genai"; -// Model seçimi (bilinçli): rapor tek seferlik ve yüksek değerli -> Opus 4.8; +export type Saglayici = "anthropic" | "gemini"; + +/** AI_SAGLAYICI env'i ile seçilir; tanımsızsa hangi anahtar varsa o kullanılır. */ +export function saglayici(): Saglayici { + const secim = process.env.AI_SAGLAYICI?.trim().toLowerCase(); + if (secim === "gemini" || secim === "anthropic") return secim; + return process.env.GEMINI_API_KEY ? "gemini" : "anthropic"; +} + +/** Seçili sağlayıcının anahtarı tanımlı mı (mock moduna düşülecek mi)? */ +export function anahtarVar(): boolean { + return saglayici() === "gemini" + ? Boolean(process.env.GEMINI_API_KEY) + : Boolean(process.env.ANTHROPIC_API_KEY || process.env.ANTHROPIC_AUTH_TOKEN); +} + +// Model seçimi (bilinçli): +// Anthropic — rapor tek seferlik ve yüksek değerli -> Opus 4.8; // sohbet kredi başına marj hassas -> Haiku 4.5 (kullanıcı kararı, 22 Tem 2026). -export const RAPOR_MODEL = "claude-opus-4-8"; -export const SOHBET_MODEL = "claude-haiku-4-5"; +// Gemini — ücretsiz katmanda yalnızca Flash ailesi açık (Pro'da kota 0), +// bu yüzden rapor ve sohbet aynı modeli kullanır; ayrım thinking seviyesinde. +const MODELLER: Record = { + anthropic: { rapor: "claude-opus-4-8", sohbet: "claude-haiku-4-5" }, + gemini: { rapor: "gemini-3.6-flash", sohbet: "gemini-3.6-flash" }, +}; -const globalForAi = globalThis as unknown as { __anthropic?: Anthropic }; +export function raporModeli(): string { + return MODELLER[saglayici()].rapor; +} + +export function sohbetModeli(): string { + return MODELLER[saglayici()].sohbet; +} + +const globalForAi = globalThis as unknown as { + __anthropic?: Anthropic; + __gemini?: GoogleGenAI; +}; export function getAnthropic(): Anthropic { if (!process.env.ANTHROPIC_API_KEY && !process.env.ANTHROPIC_AUTH_TOKEN) { @@ -17,5 +50,16 @@ export function getAnthropic(): Anthropic { return globalForAi.__anthropic; } +export function getGemini(): GoogleGenAI { + const apiKey = process.env.GEMINI_API_KEY; + if (!apiKey) { + throw new Error("AI_KEY_MISSING"); + } + if (!globalForAi.__gemini) { + globalForAi.__gemini = new GoogleGenAI({ apiKey }); + } + return globalForAi.__gemini; +} + export const SORUMLULUK_REDDI = "KolayTercih bir karar destek aracıdır; öneriler resmî YÖK Atlas verisine dayanır ancak yerleşme garantisi verilmez. Nihai tercih sorumluluğu adaya aittir."; diff --git a/src/lib/ai/rapor.ts b/src/lib/ai/rapor.ts index 9ca4897..3fd5afa 100644 --- a/src/lib/ai/rapor.ts +++ b/src/lib/ai/rapor.ts @@ -1,6 +1,6 @@ import { z } from "zod"; -import { zodOutputFormat } from "@anthropic-ai/sdk/helpers/zod"; -import { getAnthropic, RAPOR_MODEL } from "./client"; +import { anahtarVar } from "./client"; +import { yapilandirilmisUret } from "./cagri"; import { havuzOlustur, havuzuKompaktJson, @@ -88,11 +88,7 @@ function programBilgisi(p: AdayProgram): RaporSonuc["programlar"][string] { // gerçek Yapay Zeka olmadan uçtan uca test edilebilsin diye. Prod'da asla çalışmaz. function mockAktif(): boolean { - return ( - process.env.NODE_ENV !== "production" && - !process.env.ANTHROPIC_API_KEY && - !process.env.ANTHROPIC_AUTH_TOKEN - ); + return process.env.NODE_ENV !== "production" && !anahtarVar(); } function mockRapor( @@ -140,7 +136,7 @@ function mockRapor( return { tercihler, - genelDegerlendirme: `(Dev mock — ANTHROPIC_API_KEY tanımlı değil, bu rapor gerçek Yapay Zeka çıktısı değildir.) ${params.sira.toLocaleString( + genelDegerlendirme: `(Dev mock — Yapay Zeka anahtarı tanımlı değil, bu rapor gerçek Yapay Zeka çıktısı değildir.) ${params.sira.toLocaleString( "tr-TR", )}. sıra için hayal/dengeli/garanti dağılımlı 24 tercihlik örnek liste kuruldu.${ revizyonNotu ? ` Revizyon isteğin not alındı: "${revizyonNotu}".` : "" @@ -189,7 +185,6 @@ export async function raporUret( return mockRapor(havuz, params, revizyonNotu); } - const client = getAnthropic(); const havuzJson = havuzuKompaktJson(havuz); const kullaniciMesaji = [ `Aday bilgisi: başarı sıralaması ${params.sira.toLocaleString("tr-TR")}, puan türü ${turLabel(params.tur)}.`, @@ -215,24 +210,16 @@ export async function raporUret( let sonuc: z.infer | null = null; let sonHata = ""; for (let deneme = 0; deneme < 2; deneme++) { - const response = await client.messages.parse({ - model: RAPOR_MODEL, - max_tokens: 16000, - thinking: { type: "adaptive" }, + const parsed = await yapilandirilmisUret({ system: SISTEM, - messages: [ - { - role: "user", - content: - deneme === 0 - ? kullaniciMesaji - : `${kullaniciMesaji}\n\nÖNCEKİ DENEMENDE HATA VARDI: ${sonHata} Kurallara tam uy.`, - }, - ], - output_config: { format: zodOutputFormat(RaporSchema) }, + kullanici: + deneme === 0 + ? kullaniciMesaji + : `${kullaniciMesaji}\n\nÖNCEKİ DENEMENDE HATA VARDI: ${sonHata} Kurallara tam uy.`, + sema: RaporSchema, + maxTokens: 16000, }); - const parsed = response.parsed_output; if (!parsed) { sonHata = "Çıktı şemaya uymadı."; continue; diff --git a/src/lib/eposta.ts b/src/lib/eposta.ts index 61c3f86..53ff2e1 100644 --- a/src/lib/eposta.ts +++ b/src/lib/eposta.ts @@ -3,14 +3,15 @@ * * Kurallar: mail istemcileri harici CSS,