sosa123454321 commited on
Commit
088f665
·
verified ·
1 Parent(s): 9258f9c

Colab OA PDF downloader, extractor and RAG output bundle

Browse files
README.md CHANGED
@@ -22,7 +22,7 @@ language:
22
 
23
  [![Open In Colab](https://colab.research.google.com/assets/colab-badge.svg)](https://colab.research.google.com/github/SBZ-EDU/dual-diagnosis-rag/blob/master/notebooks/Dual_Diagnosis_RAG_Colab.ipynb)
24
 
25
- **اجرای یک‌کلیکی Colab:** دریافت ۱۰۰ مقاله بین‌المللی + ۱۰۰ مقاله وابسته به ایران + راهنماها از Hugging Face، آپلود PDF/کتاب مجاز، بازسازی ایندکس، آزمون خودکار سلامت RAG و ثبت W&B.
26
 
27
  > سایکوز / اسکیزوفرنی + اختلال مصرف مواد + BPD ± ADHD
28
  > مبتنی بر شواهد علمی NICE · APA · WFSBP
 
22
 
23
  [![Open In Colab](https://colab.research.google.com/assets/colab-badge.svg)](https://colab.research.google.com/github/SBZ-EDU/dual-diagnosis-rag/blob/master/notebooks/Dual_Diagnosis_RAG_Colab.ipynb)
24
 
25
+ **اجرای یک‌کلیکی Colab:** دریافت دیتاستها، دانلود خودکار PDFهای Open Access با سقف حجم، استخراج متن، آپلود PDF/کتاب مجاز، بازسازی و آزمون RAG، Fine-tune اختیاری، ثبت W&B و تحویل ZIP خروجی.
26
 
27
  > سایکوز / اسکیزوفرنی + اختلال مصرف مواد + BPD ± ADHD
28
  > مبتنی بر شواهد علمی NICE · APA · WFSBP
cloudflare/src/index.js CHANGED
@@ -17,7 +17,7 @@ async function scanPapers(env){const q=encodeURIComponent('dual diagnosis psycho
17
  async function papersFor(env,q){const lower=(q||'').toLowerCase(),map={'روان':'psychosis','سایکوز':'psychosis','ماده':'substance','اعتیاد':'addiction','لغزش':'relapse','خودکشی':'suicide','کانابیس':'cannabis','حشیش':'cannabis','درمان':'treatment','خانواده':'family','اوردوز':'overdose'};const expanded=lower+' '+Object.entries(map).filter(([fa])=>lower.includes(fa)).map(x=>x[1]).join(' ');const terms=expanded.split(/\s+/).filter(x=>x.length>3).slice(-6);if(!terms.length)return (await env.DB.prepare('SELECT title,abstract,journal,year AS published,url,corpus,treatment_tags FROM research_papers ORDER BY cited_by_count DESC LIMIT 5').all()).results;const where=terms.map(()=>'(lower(title) LIKE ? OR lower(abstract) LIKE ? OR lower(treatment_tags) LIKE ?)').join(' OR '),bind=terms.flatMap(t=>[`%${t}%`,`%${t}%`,`%${t}%`]);const found=(await env.DB.prepare(`SELECT title,abstract,journal,year AS published,url,corpus,treatment_tags FROM research_papers WHERE ${where} ORDER BY cited_by_count DESC LIMIT 5`).bind(...bind).all()).results;return found.length?found:(await env.DB.prepare('SELECT title,abstract,journal,year AS published,url,corpus,treatment_tags FROM research_papers ORDER BY cited_by_count DESC LIMIT 5').all()).results}
18
  async function guidelinesFor(env,q){const lower=(q||'').toLowerCase(),keys=['opioid','methadone','stimulant','overdose','harm','psychosis','treatment','الکل','متادون','افیون','محرک','اوردوز','کاهش آسیب'];const terms=keys.filter(x=>lower.includes(x));if(!terms.length)return (await env.GUIDELINES_DB.prepare('SELECT title,organization,year,url,tags,status FROM guidelines ORDER BY year DESC LIMIT 3').all()).results;const where=terms.map(()=>'(lower(title) LIKE ? OR lower(tags) LIKE ?)').join(' OR '),bind=terms.flatMap(t=>[`%${t}%`,`%${t}%`]);return (await env.GUIDELINES_DB.prepare(`SELECT title,organization,year,url,tags,status FROM guidelines WHERE ${where} ORDER BY year DESC LIMIT 3`).bind(...bind).all()).results}
19
  async function handle(req,env){const u=new URL(req.url);if(req.method==='OPTIONS')return new Response(null,{headers:{'access-control-allow-origin':'*','access-control-allow-headers':'content-type'}});if(u.pathname==='/')return new Response(HTML,{headers:{'content-type':'text/html;charset=utf-8','x-content-type-options':'nosniff'}});
20
- if(u.pathname==='/api/dashboard'){const [a,o,h,al,ar,p]=await Promise.all([env.DB.prepare('SELECT count(*) n FROM assessments').first(),env.DB.prepare("SELECT count(*) n FROM alerts WHERE status='open'").first(),env.DB.prepare("SELECT count(*) n FROM assessments WHERE level IN ('بالا','خیلی بالا','بحرانی')").first(),env.DB.prepare('SELECT created_at,patient_id,score,level FROM assessments ORDER BY created_at DESC LIMIT 30').all(),env.DB.prepare("SELECT created_at,patient_id,level,status FROM alerts WHERE status='open' ORDER BY created_at DESC LIMIT 30").all(),env.DB.prepare('SELECT count(*) n FROM articles').first()]);return json({stats:{assessments:a.n,openAlerts:o.n,highRisk:h.n,articles:p.n},assessments:al.results,alerts:ar.results})}
21
  if(u.pathname==='/api/data-overview'){const [research,intl,iran,live,learning,certs,guides]=await Promise.all([env.DB.prepare('SELECT count(*) n FROM research_papers').first(),env.DB.prepare("SELECT count(*) n FROM research_papers WHERE corpus='international'").first(),env.DB.prepare("SELECT count(*) n FROM research_papers WHERE corpus='iran_affiliated'").first(),env.DB.prepare('SELECT count(*) n FROM articles').first(),env.DB.prepare('SELECT count(*) n FROM learning_progress').first(),env.DB.prepare('SELECT count(*) n FROM certificates').first(),env.GUIDELINES_DB.prepare('SELECT count(*) n FROM guidelines').first()]);return json({databases:[{name:'dual-diagnosis-rag-db',kind:'clinical_and_rag',tables:8,counts:{research:research.n,international:intl.n,iran_affiliated:iran.n,live_articles:live.n,learning_progress:learning.n,certificates:certs.n}},{name:'dual-diagnosis-guidelines-db',kind:'standards',tables:1,counts:{guidelines:guides.n}},{name:'Hugging Face Dataset',kind:'portable_dataset',url:'https://huggingface.co/datasets/sosa123454321/dual-diagnosis-dataset'}]})}
22
  if(u.pathname==='/api/articles')return json({articles:(await env.DB.prepare('SELECT title,journal,published,url,source FROM articles ORDER BY published DESC LIMIT 50').all()).results});
23
  if(u.pathname==='/api/research'){const corpus=['international','iran_affiliated'].includes(u.searchParams.get('corpus'))?u.searchParams.get('corpus'):null,tag=(u.searchParams.get('tag')||'').toLowerCase(),lang=u.searchParams.get('language'),limit=Math.min(100,Math.max(1,+u.searchParams.get('limit')||25));let sql='SELECT title,year,language,journal,url,open_access,corpus,treatment_tags,cited_by_count FROM research_papers WHERE 1=1',args=[];if(corpus){sql+=' AND corpus=?';args.push(corpus)}if(lang){sql+=' AND language=?';args.push(lang)}if(tag){sql+=' AND lower(treatment_tags) LIKE ?';args.push('%'+tag+'%')}sql+=' ORDER BY cited_by_count DESC LIMIT ?';args.push(limit);const rows=(await env.DB.prepare(sql).bind(...args).all()).results;return json({count:rows.length,filters:{corpus,language:lang,tag},papers:rows})}
@@ -28,7 +28,7 @@ if(req.method==='POST'&&u.pathname==='/api/academy/complete'){const d=await req.
28
  if(req.method==='POST'&&u.pathname==='/api/academy/certificate'){const d=await req.json();if(!d.learnerRef||!['patient','family'].includes(d.audience))return json({error:'اطلاعات ناقص است'},400);const learner=await hash(env.PATIENT_SALT+':academy:'+d.learnerRef),required=MODULES.filter(x=>x.audience===d.audience).length,count=(await env.DB.prepare('SELECT count(*) n FROM learning_progress WHERE learner_id=? AND audience=? AND score=100').bind(learner,d.audience).first()).n;if(count<required)return json({error:`ابتدا همه ${required} ماژول را تکمیل کنید.`,completed:count},400);const existing=await env.DB.prepare('SELECT id,verification_code,issued_at FROM certificates WHERE learner_id=? AND audience=?').bind(learner,d.audience).first();if(existing)return json({certificate:existing});const id=crypto.randomUUID(),code=(await hash(id+learner)).slice(0,12).toUpperCase(),issued=new Date().toISOString();await env.DB.prepare('INSERT INTO certificates VALUES(?,?,?,?,?,?)').bind(id,learner,d.audience,issued,count,code).run();return json({certificate:{id,verification_code:code,issued_at:issued},notice:'گواهی مشارکت آموزشی؛ فاقد اعتبار حرفه‌ای یا درمانی.'})}
29
  if(u.pathname.startsWith('/api/certificate/')){const code=u.pathname.split('/').pop(),c=await env.DB.prepare('SELECT audience,issued_at,modules_count,verification_code FROM certificates WHERE verification_code=?').bind(code).first();return c?json({valid:true,...c,notice:'Educational participation certificate; not a clinical license.'}):json({valid:false},404)}
30
  if(req.method==='POST'&&u.pathname==='/api/admin/scan'){if(req.headers.get('authorization')!==`Bearer ${env.ADMIN_TOKEN}`)return json({error:'unauthorized'},401);return json({imported:await scanPapers(env)})}
31
- if(req.method==='POST'&&u.pathname==='/api/risk'){const d=await req.json();let score=0;for(const k in W)score+=Math.max(0,Math.min(4,+d[k]||0))*W[k];let level=score>=45?'خیلی بالا':score>=28?'بالا':score>=14?'متوسط':'پایین';if(+d.suicide===4||+d.withdrawal===4)level='بحرانی';const actions={بحرانی:'ارزیابی فوری حضوری/اورژانس؛ بیمار تنها نماند.','خیلی بالا':'بازبینی همان‌روز توسط روان‌پزشک.',بالا:'تماس با تیم درمان طی ۲۴ ساعت.',متوسط:'بازبینی بالینی طی ۷۲ ساعت.',پایین:'ادامه پایش برنامه‌ریزی‌شده.'};const pid=await hash(env.PATIENT_SALT+':'+(d.patientRef||crypto.randomUUID())),id=crypto.randomUUID(),now=new Date().toISOString();await env.DB.prepare('INSERT INTO assessments VALUES(?,?,?,?,?,?)').bind(id,pid,now,score,level,JSON.stringify(d)).run();if(['بالا','خیلی بالا','بحرانی'].includes(level))await env.DB.prepare('INSERT INTO alerts VALUES(?,?,?,?,?,?)').bind(crypto.randomUUID(),id,pid,now,level,'open').run();return json({id,score,level,action:actions[level],storage:'Cloudflare D1',disclaimer:'جایگزین ارزیابی پزشک نیست.'})}
32
  if(req.method==='POST'&&u.pathname==='/api/chat'){const started=Date.now(),body=await req.json(),question=body.question,role=['admin','doctor','family','patient'].includes(body.role)?body.role:'patient';if(!question)return json({error:'لطفاً سؤال را بنویسید.'},400);const key=await hash(role+':'+question.trim().toLowerCase()),cached=await env.DB.prepare("SELECT answer,source FROM rag_cache WHERE cache_key=? AND created_at>datetime('now','-7 days')").bind(key).first();const [papers,guidelines]=await Promise.all([papersFor(env,question),guidelinesFor(env,question)]),context=(papers.map((p,i)=>`مقاله ${i+1}. ${p.title} (${p.journal}, ${p.published}) ${p.abstract||''} URL:${p.url}`).join('\n')+'\n'+guidelines.map((g,i)=>`استاندارد ${i+1}. ${g.title} (${g.organization}, ${g.year}) URL:${g.url} status:${g.status}`).join('\n')).slice(0,10000),roleGuide={patient:'برای بیمار با زبان ساده، امیدبخش و بدون دوز یا جزئیات حساس پاسخ بده.',family:'برای خانواده، اقدامات حمایتی، علائم هشدار و زمان تماس با پزشک را ساده توضیح بده؛ دوز نده.',doctor:'برای پزشک با زبان فنی، محدودیت شواهد و DOIها پاسخ بده؛ دوز فقط اگر در شواهد تأییدشده حاضر است و هرگز نسخه فردی نده.',admin:'فقط درباره عملکرد سامانه، داده، منابع و پایش پاسخ بده؛ توصیه درمانی نده.'},messages=[{role:'system',content:`به فارسی و فقط بر اساس شواهد داده‌شده پاسخ بده. شماره مقاله را ذکر کن، عدم قطعیت را صریح بگو و جایگزین پزشک نشو. ${roleGuide[role]}`},{role:'user',content:`نقش مخاطب: ${role}\nپرسش: ${question}\nشواهد:\n${context}`}];let answer,provider='Cloudflare Workers AI';try{const x=await env.AI.run('@cf/meta/llama-3.1-8b-instruct-fast',{messages,max_tokens:500});answer=x.response}catch(e){provider='Hugging Face RAG';try{const r=await fetch('https://router.huggingface.co/v1/chat/completions',{method:'POST',headers:{Authorization:'Bearer '+env.HF_TOKEN,'content-type':'application/json'},body:JSON.stringify({model:env.HF_MODEL,messages,max_tokens:500})});const x=await r.json();answer=x.choices?.[0]?.message?.content}catch{}}if(!answer){answer=baseAnswer(question);provider='RAG داخلی'}await Promise.all([env.DB.prepare('INSERT OR REPLACE INTO rag_cache VALUES(?,?,?,?)').bind(key,answer,provider,new Date().toISOString()).run(),env.DB.prepare('INSERT INTO api_usage(endpoint,provider,latency_ms,success,created_at) VALUES(?,?,?,?,?)').bind('/api/chat',provider,Date.now()-started,1,new Date().toISOString()).run()]);return json({answer,role,source:provider,papers,guidelines,latency_ms:Date.now()-started})}
33
  return json({error:'مسیر پیدا نشد'},404)}
34
  export default{fetch(req,env){return handle(req,env).catch(e=>json({error:'خطای داخلی',detail:String(e)},500))},async scheduled(event,env,ctx){ctx.waitUntil(scanPapers(env))}};
 
17
  async function papersFor(env,q){const lower=(q||'').toLowerCase(),map={'روان':'psychosis','سایکوز':'psychosis','ماده':'substance','اعتیاد':'addiction','لغزش':'relapse','خودکشی':'suicide','کانابیس':'cannabis','حشیش':'cannabis','درمان':'treatment','خانواده':'family','اوردوز':'overdose'};const expanded=lower+' '+Object.entries(map).filter(([fa])=>lower.includes(fa)).map(x=>x[1]).join(' ');const terms=expanded.split(/\s+/).filter(x=>x.length>3).slice(-6);if(!terms.length)return (await env.DB.prepare('SELECT title,abstract,journal,year AS published,url,corpus,treatment_tags FROM research_papers ORDER BY cited_by_count DESC LIMIT 5').all()).results;const where=terms.map(()=>'(lower(title) LIKE ? OR lower(abstract) LIKE ? OR lower(treatment_tags) LIKE ?)').join(' OR '),bind=terms.flatMap(t=>[`%${t}%`,`%${t}%`,`%${t}%`]);const found=(await env.DB.prepare(`SELECT title,abstract,journal,year AS published,url,corpus,treatment_tags FROM research_papers WHERE ${where} ORDER BY cited_by_count DESC LIMIT 5`).bind(...bind).all()).results;return found.length?found:(await env.DB.prepare('SELECT title,abstract,journal,year AS published,url,corpus,treatment_tags FROM research_papers ORDER BY cited_by_count DESC LIMIT 5').all()).results}
18
  async function guidelinesFor(env,q){const lower=(q||'').toLowerCase(),keys=['opioid','methadone','stimulant','overdose','harm','psychosis','treatment','الکل','متادون','افیون','محرک','اوردوز','کاهش آسیب'];const terms=keys.filter(x=>lower.includes(x));if(!terms.length)return (await env.GUIDELINES_DB.prepare('SELECT title,organization,year,url,tags,status FROM guidelines ORDER BY year DESC LIMIT 3').all()).results;const where=terms.map(()=>'(lower(title) LIKE ? OR lower(tags) LIKE ?)').join(' OR '),bind=terms.flatMap(t=>[`%${t}%`,`%${t}%`]);return (await env.GUIDELINES_DB.prepare(`SELECT title,organization,year,url,tags,status FROM guidelines WHERE ${where} ORDER BY year DESC LIMIT 3`).bind(...bind).all()).results}
19
  async function handle(req,env){const u=new URL(req.url);if(req.method==='OPTIONS')return new Response(null,{headers:{'access-control-allow-origin':'*','access-control-allow-headers':'content-type'}});if(u.pathname==='/')return new Response(HTML,{headers:{'content-type':'text/html;charset=utf-8','x-content-type-options':'nosniff'}});
20
+ if(u.pathname==='/api/dashboard'){const [a,o,h,al,ar,p]=await Promise.all([env.DB.prepare('SELECT count(*) n FROM assessments').first(),env.DB.prepare("SELECT count(*) n FROM alerts WHERE status='open'").first(),env.DB.prepare("SELECT count(*) n FROM assessments WHERE level IN ('بالا','خیلی بالا','بحرانی')").first(),env.DB.prepare("SELECT created_at,substr(patient_id,1,8)||'…' AS patient_id,score,level FROM assessments ORDER BY created_at DESC LIMIT 30").all(),env.DB.prepare("SELECT created_at,substr(patient_id,1,8)||'…' AS patient_id,level,status FROM alerts WHERE status='open' ORDER BY created_at DESC LIMIT 30").all(),env.DB.prepare('SELECT count(*) n FROM articles').first()]);return json({stats:{assessments:a.n,openAlerts:o.n,highRisk:h.n,articles:p.n},assessments:al.results,alerts:ar.results})}
21
  if(u.pathname==='/api/data-overview'){const [research,intl,iran,live,learning,certs,guides]=await Promise.all([env.DB.prepare('SELECT count(*) n FROM research_papers').first(),env.DB.prepare("SELECT count(*) n FROM research_papers WHERE corpus='international'").first(),env.DB.prepare("SELECT count(*) n FROM research_papers WHERE corpus='iran_affiliated'").first(),env.DB.prepare('SELECT count(*) n FROM articles').first(),env.DB.prepare('SELECT count(*) n FROM learning_progress').first(),env.DB.prepare('SELECT count(*) n FROM certificates').first(),env.GUIDELINES_DB.prepare('SELECT count(*) n FROM guidelines').first()]);return json({databases:[{name:'dual-diagnosis-rag-db',kind:'clinical_and_rag',tables:8,counts:{research:research.n,international:intl.n,iran_affiliated:iran.n,live_articles:live.n,learning_progress:learning.n,certificates:certs.n}},{name:'dual-diagnosis-guidelines-db',kind:'standards',tables:1,counts:{guidelines:guides.n}},{name:'Hugging Face Dataset',kind:'portable_dataset',url:'https://huggingface.co/datasets/sosa123454321/dual-diagnosis-dataset'}]})}
22
  if(u.pathname==='/api/articles')return json({articles:(await env.DB.prepare('SELECT title,journal,published,url,source FROM articles ORDER BY published DESC LIMIT 50').all()).results});
23
  if(u.pathname==='/api/research'){const corpus=['international','iran_affiliated'].includes(u.searchParams.get('corpus'))?u.searchParams.get('corpus'):null,tag=(u.searchParams.get('tag')||'').toLowerCase(),lang=u.searchParams.get('language'),limit=Math.min(100,Math.max(1,+u.searchParams.get('limit')||25));let sql='SELECT title,year,language,journal,url,open_access,corpus,treatment_tags,cited_by_count FROM research_papers WHERE 1=1',args=[];if(corpus){sql+=' AND corpus=?';args.push(corpus)}if(lang){sql+=' AND language=?';args.push(lang)}if(tag){sql+=' AND lower(treatment_tags) LIKE ?';args.push('%'+tag+'%')}sql+=' ORDER BY cited_by_count DESC LIMIT ?';args.push(limit);const rows=(await env.DB.prepare(sql).bind(...args).all()).results;return json({count:rows.length,filters:{corpus,language:lang,tag},papers:rows})}
 
28
  if(req.method==='POST'&&u.pathname==='/api/academy/certificate'){const d=await req.json();if(!d.learnerRef||!['patient','family'].includes(d.audience))return json({error:'اطلاعات ناقص است'},400);const learner=await hash(env.PATIENT_SALT+':academy:'+d.learnerRef),required=MODULES.filter(x=>x.audience===d.audience).length,count=(await env.DB.prepare('SELECT count(*) n FROM learning_progress WHERE learner_id=? AND audience=? AND score=100').bind(learner,d.audience).first()).n;if(count<required)return json({error:`ابتدا همه ${required} ماژول را تکمیل کنید.`,completed:count},400);const existing=await env.DB.prepare('SELECT id,verification_code,issued_at FROM certificates WHERE learner_id=? AND audience=?').bind(learner,d.audience).first();if(existing)return json({certificate:existing});const id=crypto.randomUUID(),code=(await hash(id+learner)).slice(0,12).toUpperCase(),issued=new Date().toISOString();await env.DB.prepare('INSERT INTO certificates VALUES(?,?,?,?,?,?)').bind(id,learner,d.audience,issued,count,code).run();return json({certificate:{id,verification_code:code,issued_at:issued},notice:'گواهی مشارکت آموزشی؛ فاقد اعتبار حرفه‌ای یا درمانی.'})}
29
  if(u.pathname.startsWith('/api/certificate/')){const code=u.pathname.split('/').pop(),c=await env.DB.prepare('SELECT audience,issued_at,modules_count,verification_code FROM certificates WHERE verification_code=?').bind(code).first();return c?json({valid:true,...c,notice:'Educational participation certificate; not a clinical license.'}):json({valid:false},404)}
30
  if(req.method==='POST'&&u.pathname==='/api/admin/scan'){if(req.headers.get('authorization')!==`Bearer ${env.ADMIN_TOKEN}`)return json({error:'unauthorized'},401);return json({imported:await scanPapers(env)})}
31
+ if(req.method==='POST'&&u.pathname==='/api/risk'){const d=await req.json();let score=0;for(const k in W)score+=Math.max(0,Math.min(4,+d[k]||0))*W[k];let level=score>=45?'خیلی بالا':score>=28?'بالا':score>=14?'متوسط':'پایین';if(+d.suicide===4||+d.withdrawal===4)level='بحرانی';const actions={بحرانی:'ارزیابی فوری حضوری/اورژانس؛ بیمار تنها نماند.','خیلی بالا':'بازبینی همان‌روز توسط روان‌پزشک.',بالا:'تماس با تیم درمان طی ۲۴ ساعت.',متوسط:'بازبینی بالینی طی ۷۲ ساعت.',پایین:'ادامه پایش برنامه‌ریزی‌شده.'};const ref=String(d.patientRef||crypto.randomUUID()).slice(0,128),pid=await hash(env.PATIENT_SALT+':'+ref),id=crypto.randomUUID(),now=new Date().toISOString(),safePayload=Object.fromEntries(Object.keys(W).map(k=>[k,Math.max(0,Math.min(4,+d[k]||0))]));await env.DB.prepare('INSERT INTO assessments VALUES(?,?,?,?,?,?)').bind(id,pid,now,score,level,JSON.stringify(safePayload)).run();if(['بالا','خیلی بالا','بحرانی'].includes(level))await env.DB.prepare('INSERT INTO alerts VALUES(?,?,?,?,?,?)').bind(crypto.randomUUID(),id,pid,now,level,'open').run();return json({id,score,level,action:actions[level],storage:'Cloudflare D1',disclaimer:'جایگزین ارزیابی پزشک نیست.'})}
32
  if(req.method==='POST'&&u.pathname==='/api/chat'){const started=Date.now(),body=await req.json(),question=body.question,role=['admin','doctor','family','patient'].includes(body.role)?body.role:'patient';if(!question)return json({error:'لطفاً سؤال را بنویسید.'},400);const key=await hash(role+':'+question.trim().toLowerCase()),cached=await env.DB.prepare("SELECT answer,source FROM rag_cache WHERE cache_key=? AND created_at>datetime('now','-7 days')").bind(key).first();const [papers,guidelines]=await Promise.all([papersFor(env,question),guidelinesFor(env,question)]),context=(papers.map((p,i)=>`مقاله ${i+1}. ${p.title} (${p.journal}, ${p.published}) ${p.abstract||''} URL:${p.url}`).join('\n')+'\n'+guidelines.map((g,i)=>`استاندارد ${i+1}. ${g.title} (${g.organization}, ${g.year}) URL:${g.url} status:${g.status}`).join('\n')).slice(0,10000),roleGuide={patient:'برای بیمار با زبان ساده، امیدبخش و بدون دوز یا جزئیات حساس پاسخ بده.',family:'برای خانواده، اقدامات حمایتی، علائم هشدار و زمان تماس با پزشک را ساده توضیح بده؛ دوز نده.',doctor:'برای پزشک با زبان فنی، محدودیت شواهد و DOIها پاسخ بده؛ دوز فقط اگر در شواهد تأییدشده حاضر است و هرگز نسخه فردی نده.',admin:'فقط درباره عملکرد سامانه، داده، منابع و پایش پاسخ بده؛ توصیه درمانی نده.'},messages=[{role:'system',content:`به فارسی و فقط بر اساس شواهد داده‌شده پاسخ بده. شماره مقاله را ذکر کن، عدم قطعیت را صریح بگو و جایگزین پزشک نشو. ${roleGuide[role]}`},{role:'user',content:`نقش مخاطب: ${role}\nپرسش: ${question}\nشواهد:\n${context}`}];let answer,provider='Cloudflare Workers AI';try{const x=await env.AI.run('@cf/meta/llama-3.1-8b-instruct-fast',{messages,max_tokens:500});answer=x.response}catch(e){provider='Hugging Face RAG';try{const r=await fetch('https://router.huggingface.co/v1/chat/completions',{method:'POST',headers:{Authorization:'Bearer '+env.HF_TOKEN,'content-type':'application/json'},body:JSON.stringify({model:env.HF_MODEL,messages,max_tokens:500})});const x=await r.json();answer=x.choices?.[0]?.message?.content}catch{}}if(!answer){answer=baseAnswer(question);provider='RAG داخلی'}await Promise.all([env.DB.prepare('INSERT OR REPLACE INTO rag_cache VALUES(?,?,?,?)').bind(key,answer,provider,new Date().toISOString()).run(),env.DB.prepare('INSERT INTO api_usage(endpoint,provider,latency_ms,success,created_at) VALUES(?,?,?,?,?)').bind('/api/chat',provider,Date.now()-started,1,new Date().toISOString()).run()]);return json({answer,role,source:provider,papers,guidelines,latency_ms:Date.now()-started})}
33
  return json({error:'مسیر پیدا نشد'},404)}
34
  export default{fetch(req,env){return handle(req,env).catch(e=>json({error:'خطای داخلی',detail:String(e)},500))},async scheduled(event,env,ctx){ctx.waitUntil(scanPapers(env))}};
notebooks/Dual_Diagnosis_RAG_Colab.ipynb CHANGED
@@ -89,7 +89,72 @@
89
  "cell_type": "markdown",
90
  "metadata": {},
91
  "source": [
92
- "## 4) اختیاری: آپلود PDF مقاله/کتاب مجاز\n",
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
93
  "PDFها به متن محلی تبدیل می‌شوند و فایل اصلی به Hugging Face یا W&B ارسال نمی‌شود مگر خودتان صریحاً این کار را انجام دهید."
94
  ]
95
  },
@@ -120,7 +185,7 @@
120
  "cell_type": "markdown",
121
  "metadata": {},
122
  "source": [
123
- "## 5) ساخت ایندکس برداری RAG"
124
  ]
125
  },
126
  {
@@ -141,7 +206,7 @@
141
  "cell_type": "markdown",
142
  "metadata": {},
143
  "source": [
144
- "## 6) تست بازیابی مقاله"
145
  ]
146
  },
147
  {
@@ -161,7 +226,7 @@
161
  "cell_type": "markdown",
162
  "metadata": {},
163
  "source": [
164
- "## 7) پاسخ RAG\n",
165
  "حالت پیش‌فرض استخراجی و رایگان است. برای مدل مولد، GPU را از Runtime → Change runtime type فعال کنید."
166
  ]
167
  },
@@ -183,7 +248,7 @@
183
  "cell_type": "markdown",
184
  "metadata": {},
185
  "source": [
186
- "## 8) ارزیابی خودکار سلامت RAG\n",
187
  "این تست بررسی می‌کند که ایندکس خالی نباشد، ابعاد بردار درست باشد، منابع مقاله/راهنما وارد شده باشند و برای چند پرسش نمونه نتیجه برگردد. این آزمون فنی است، نه اعتبارسنجی بالینی."
188
  ]
189
  },
@@ -220,7 +285,7 @@
220
  "cell_type": "markdown",
221
  "metadata": {},
222
  "source": [
223
- "## 9) ثبت ارزیابی در W&B (اختیاری)\n",
224
  "کلید را فقط در Colab Secrets با نام `WANDB_API_KEY` ذخیره کنید؛ آن را داخل نوت‌بوک ننویسید."
225
  ]
226
  },
@@ -248,7 +313,7 @@
248
  "cell_type": "markdown",
249
  "metadata": {},
250
  "source": [
251
- "## 10) Fine-tune اختیاری LoRA روی GPU\n",
252
  "این مرحله وزن‌های پایه را تغییر نمی‌دهد؛ یک Adapter سبک می‌سازد. فقط داده‌های تأییدشده را برای آموزش استفاده کنید. مقاله خام بدون تبدیل به نمونه پرسش/پاسخ برای Fine-tune مناسب نیست."
253
  ]
254
  },
@@ -296,7 +361,7 @@
296
  "cell_type": "markdown",
297
  "metadata": {},
298
  "source": [
299
- "## 11) اجرای رابط Gradio در Colab (اختیاری)"
300
  ]
301
  },
302
  {
@@ -310,6 +375,33 @@
310
  "!USE_GENERATOR=0 GRADIO_SHARE=1 python app.py\n"
311
  ]
312
  },
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
313
  {
314
  "cell_type": "markdown",
315
  "metadata": {},
 
89
  "cell_type": "markdown",
90
  "metadata": {},
91
  "source": [
92
+ "## 4) دانلود خودکار PDFهای Open Access و استخراج متن\n",
93
+ "این بخش فقط URLهایی را دریافت می‌کند که در Manifest به‌عنوان Open Access ثبت شده‌اند؛ Paywall دور زده نمی‌شود. برای جلوگیری از پرشدن فضای Colab، سقف تعداد و حجم قابل تنظیم است."
94
+ ]
95
+ },
96
+ {
97
+ "cell_type": "code",
98
+ "execution_count": null,
99
+ "metadata": {},
100
+ "outputs": [],
101
+ "source": [
102
+ "import requests, hashlib, time\n",
103
+ "from pathlib import Path\n",
104
+ "manifest_cache=hf_hub_download(repo_id=repo,repo_type='dataset',filename='articles/open_access_pdf_manifest.jsonl')\n",
105
+ "manifest=[json.loads(x) for x in open(manifest_cache,encoding='utf-8') if x.strip() and json.loads(x).get('pdf_url')]\n",
106
+ "MAX_FILES=50 # برای همه لینک‌ها تا 128 افزایش دهید\n",
107
+ "MAX_TOTAL_MB=200 # سقف دانلود\n",
108
+ "PDF_DIR=Path('/content/oa_pdfs'); PDF_DIR.mkdir(exist_ok=True)\n",
109
+ "TEXT_DIR=Path('data/articles/oa_fulltext'); TEXT_DIR.mkdir(parents=True,exist_ok=True)\n",
110
+ "selected=[]; planned=0\n",
111
+ "for x in manifest:\n",
112
+ " size=x.get('content_length_bytes') or 0\n",
113
+ " if len(selected)>=MAX_FILES: break\n",
114
+ " if size and planned+size>MAX_TOTAL_MB*1024*1024: continue\n",
115
+ " selected.append(x); planned+=size\n",
116
+ "print('Manifest PDFs:',len(manifest),'Selected:',len(selected),'Known planned MB:',round(planned/1024/1024,2))\n"
117
+ ]
118
+ },
119
+ {
120
+ "cell_type": "code",
121
+ "execution_count": null,
122
+ "metadata": {},
123
+ "outputs": [],
124
+ "source": [
125
+ "from pypdf import PdfReader\n",
126
+ "report=[]; downloaded=0\n",
127
+ "for i,x in enumerate(selected,1):\n",
128
+ " url=x['pdf_url']; key=(x.get('doi') or x.get('openalex_id') or str(i)).rsplit('/',1)[-1]\n",
129
+ " safe=hashlib.sha256(key.encode()).hexdigest()[:16]\n",
130
+ " pdf=PDF_DIR/f'{safe}.pdf'; txt=TEXT_DIR/f'{safe}.md'\n",
131
+ " try:\n",
132
+ " with requests.get(url,stream=True,timeout=45,headers={'User-Agent':'Mozilla/5.0 OpenAccessResearchBot/1.0'}) as r:\n",
133
+ " r.raise_for_status(); ctype=r.headers.get('content-type','')\n",
134
+ " if 'pdf' not in ctype.lower() and not url.lower().endswith('.pdf'): raise ValueError('response is not PDF')\n",
135
+ " with open(pdf,'wb') as f:\n",
136
+ " for chunk in r.iter_content(1024*1024):\n",
137
+ " if chunk:\n",
138
+ " if downloaded+len(chunk)>MAX_TOTAL_MB*1024*1024: raise RuntimeError('download cap reached')\n",
139
+ " f.write(chunk); downloaded+=len(chunk)\n",
140
+ " reader=PdfReader(str(pdf)); text='\\n'.join((page.extract_text() or '') for page in reader.pages)\n",
141
+ " txt.write_text(f\"# {x.get('title','Open Access Paper')}\\n\\nSource: {url}\\nDOI: {x.get('doi')}\\n\\n{text}\",encoding='utf-8')\n",
142
+ " report.append({'title':x.get('title'),'url':url,'pdf':str(pdf),'text':str(txt),'bytes':pdf.stat().st_size,'pages':len(reader.pages),'text_chars':len(text),'status':'ok'})\n",
143
+ " print(f\"[{i}/{len(selected)}] ✓\",x.get('title','')[:65],len(reader.pages),'pages')\n",
144
+ " except Exception as e:\n",
145
+ " if pdf.exists() and not txt.exists(): pdf.unlink()\n",
146
+ " report.append({'title':x.get('title'),'url':url,'status':'failed','error':str(e)[:300]})\n",
147
+ " print(f\"[{i}/{len(selected)}] ✗\",str(e)[:100])\n",
148
+ "Path('/content/pdf_download_report.json').write_text(json.dumps(report,ensure_ascii=False,indent=2),encoding='utf-8')\n",
149
+ "ok=[r for r in report if r['status']=='ok']\n",
150
+ "print({'successful':len(ok),'failed':len(report)-len(ok),'downloaded_mb':round(downloaded/1024/1024,2),'extracted_characters':sum(r.get('text_chars',0) for r in ok)})\n"
151
+ ]
152
+ },
153
+ {
154
+ "cell_type": "markdown",
155
+ "metadata": {},
156
+ "source": [
157
+ "## 5) اختیاری: آپلود PDF مقاله/کتاب مجاز\n",
158
  "PDFها به متن محلی تبدیل می‌شوند و فایل اصلی به Hugging Face یا W&B ارسال نمی‌شود مگر خودتان صریحاً این کار را انجام دهید."
159
  ]
160
  },
 
185
  "cell_type": "markdown",
186
  "metadata": {},
187
  "source": [
188
+ "## 6) ساخت ایندکس برداری RAG"
189
  ]
190
  },
191
  {
 
206
  "cell_type": "markdown",
207
  "metadata": {},
208
  "source": [
209
+ "## 7) تست بازیابی مقاله"
210
  ]
211
  },
212
  {
 
226
  "cell_type": "markdown",
227
  "metadata": {},
228
  "source": [
229
+ "## 8) پاسخ RAG\n",
230
  "حالت پیش‌فرض استخراجی و رایگان است. برای مدل مولد، GPU را از Runtime → Change runtime type فعال کنید."
231
  ]
232
  },
 
248
  "cell_type": "markdown",
249
  "metadata": {},
250
  "source": [
251
+ "## 9) ارزیابی خودکار سلامت RAG\n",
252
  "این تست بررسی می‌کند که ایندکس خالی نباشد، ابعاد بردار درست باشد، منابع مقاله/راهنما وارد شده باشند و برای چند پرسش نمونه نتیجه برگردد. این آزمون فنی است، نه اعتبارسنجی بالینی."
253
  ]
254
  },
 
285
  "cell_type": "markdown",
286
  "metadata": {},
287
  "source": [
288
+ "## 10) ثبت ارزیابی در W&B (اختیاری)\n",
289
  "کلید را فقط در Colab Secrets با نام `WANDB_API_KEY` ذخیره کنید؛ آن را داخل نوت‌بوک ننویسید."
290
  ]
291
  },
 
313
  "cell_type": "markdown",
314
  "metadata": {},
315
  "source": [
316
+ "## 11) Fine-tune اختیاری LoRA روی GPU\n",
317
  "این مرحله وزن‌های پایه را تغییر نمی‌دهد؛ یک Adapter سبک می‌سازد. فقط داده‌های تأییدشده را برای آموزش استفاده کنید. مقاله خام بدون تبدیل به نمونه پرسش/پاسخ برای Fine-tune مناسب نیست."
318
  ]
319
  },
 
361
  "cell_type": "markdown",
362
  "metadata": {},
363
  "source": [
364
+ "## 12) اجرای رابط Gradio در Colab (اختیاری)"
365
  ]
366
  },
367
  {
 
375
  "!USE_GENERATOR=0 GRADIO_SHARE=1 python app.py\n"
376
  ]
377
  },
378
+ {
379
+ "cell_type": "markdown",
380
+ "metadata": {},
381
+ "source": [
382
+ "## 13) ساخت و دانلود بسته خروجی\n",
383
+ "بسته شامل متن‌های استخراج‌شده، گزارش PDF و ایندکس برداری است. خود PDFها برای کاهش حجم ZIP نمی‌شوند؛ در `/content/oa_pdfs` باقی می‌مانند."
384
+ ]
385
+ },
386
+ {
387
+ "cell_type": "code",
388
+ "execution_count": null,
389
+ "metadata": {},
390
+ "outputs": [],
391
+ "source": [
392
+ "import shutil, os\n",
393
+ "bundle='/content/dual_diagnosis_rag_output'\n",
394
+ "shutil.rmtree(bundle,ignore_errors=True); os.makedirs(bundle)\n",
395
+ "for src in ['index/chunks.json','index/vectors.npz','/content/pdf_download_report.json']:\n",
396
+ " if os.path.exists(src): shutil.copy(src,bundle)\n",
397
+ "if TEXT_DIR.exists(): shutil.copytree(TEXT_DIR,Path(bundle)/'oa_fulltext',dirs_exist_ok=True)\n",
398
+ "archive=shutil.make_archive('/content/dual_diagnosis_rag_output','zip','/content',Path(bundle).name)\n",
399
+ "print(archive,round(os.path.getsize(archive)/1024/1024,2),'MB')\n",
400
+ "if IN_COLAB:\n",
401
+ " from google.colab import files\n",
402
+ " files.download(archive)\n"
403
+ ]
404
+ },
405
  {
406
  "cell_type": "markdown",
407
  "metadata": {},