Colab OA PDF downloader, extractor and RAG output bundle
Browse files- README.md +1 -1
- cloudflare/src/index.js +2 -2
- notebooks/Dual_Diagnosis_RAG_Colab.ipynb +100 -8
README.md
CHANGED
|
@@ -22,7 +22,7 @@ language:
|
|
| 22 |
|
| 23 |
[](https://colab.research.google.com/github/SBZ-EDU/dual-diagnosis-rag/blob/master/notebooks/Dual_Diagnosis_RAG_Colab.ipynb)
|
| 24 |
|
| 25 |
-
**اجرای یککلیکی Colab:** دریافت
|
| 26 |
|
| 27 |
> سایکوز / اسکیزوفرنی + اختلال مصرف مواد + BPD ± ADHD
|
| 28 |
> مبتنی بر شواهد علمی NICE · APA · WFSBP
|
|
|
|
| 22 |
|
| 23 |
[](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(
|
| 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+':'+
|
| 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) اخ
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 93 |
"PDFها به متن محلی تبدیل میشوند و فایل اصلی به Hugging Face یا W&B ارسال نمیشود مگر خودتان صریحاً این کار را انجام دهید."
|
| 94 |
]
|
| 95 |
},
|
|
@@ -120,7 +185,7 @@
|
|
| 120 |
"cell_type": "markdown",
|
| 121 |
"metadata": {},
|
| 122 |
"source": [
|
| 123 |
-
"##
|
| 124 |
]
|
| 125 |
},
|
| 126 |
{
|
|
@@ -141,7 +206,7 @@
|
|
| 141 |
"cell_type": "markdown",
|
| 142 |
"metadata": {},
|
| 143 |
"source": [
|
| 144 |
-
"##
|
| 145 |
]
|
| 146 |
},
|
| 147 |
{
|
|
@@ -161,7 +226,7 @@
|
|
| 161 |
"cell_type": "markdown",
|
| 162 |
"metadata": {},
|
| 163 |
"source": [
|
| 164 |
-
"##
|
| 165 |
"حالت پیشفرض استخراجی و رایگان است. برای مدل مولد، GPU را از Runtime → Change runtime type فعال کنید."
|
| 166 |
]
|
| 167 |
},
|
|
@@ -183,7 +248,7 @@
|
|
| 183 |
"cell_type": "markdown",
|
| 184 |
"metadata": {},
|
| 185 |
"source": [
|
| 186 |
-
"##
|
| 187 |
"این تست بررسی میکند که ایندکس خالی نباشد، ابعاد بردار درست باشد، منابع مقاله/راهنما وارد شده باشند و برای چند پرسش نمونه نتیجه برگردد. این آزمون فنی است، نه اعتبارسنجی بالینی."
|
| 188 |
]
|
| 189 |
},
|
|
@@ -220,7 +285,7 @@
|
|
| 220 |
"cell_type": "markdown",
|
| 221 |
"metadata": {},
|
| 222 |
"source": [
|
| 223 |
-
"##
|
| 224 |
"کلید را فقط در Colab Secrets با نام `WANDB_API_KEY` ذخیره کنید؛ آن را داخل نوتبوک ننویسید."
|
| 225 |
]
|
| 226 |
},
|
|
@@ -248,7 +313,7 @@
|
|
| 248 |
"cell_type": "markdown",
|
| 249 |
"metadata": {},
|
| 250 |
"source": [
|
| 251 |
-
"##
|
| 252 |
"این مرحله وزنهای پایه را تغییر نمیدهد؛ یک Adapter سبک میسازد. فقط دادههای تأییدشده را برای آموزش استفاده کنید. مقاله خام بدون تبدیل به نمونه پرسش/پاسخ برای Fine-tune مناسب نیست."
|
| 253 |
]
|
| 254 |
},
|
|
@@ -296,7 +361,7 @@
|
|
| 296 |
"cell_type": "markdown",
|
| 297 |
"metadata": {},
|
| 298 |
"source": [
|
| 299 |
-
"##
|
| 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": {},
|