@lzhzzzzwill/cofos 1.0.4 → 1.1.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +36 -10
- package/RUNTIME.md +38 -0
- package/bin/cofos.js +82 -20
- package/package.json +13 -5
- package/requirements.txt +0 -3
- package/runtime/config/config.yaml +73 -260
- package/runtime/config/prompt_templates.yaml +73 -369
- package/runtime/src/data_processing/__init__.py +2 -27
- package/runtime/src/data_processing/utils.py +22 -1
- package/runtime/src/inference/__init__.py +2 -6
- package/runtime/src/inference/query_router.py +23 -11
- package/runtime/src/inference/student_adapter_inference.py +312 -61
- package/runtime/src/retrieval/__init__.py +14 -7
- package/runtime/src/retrieval/retrieve_evidence.py +84 -25
- package/scripts/chat.py +112 -29
- package/scripts/sync_runtime.py +45 -0
- package/runtime/config/qwen3_5_config.md +0 -46
- package/runtime/src/data_processing/clean_json_records.py +0 -387
- package/runtime/src/data_processing/parse_wos_xlsx.py +0 -255
- package/runtime/src/data_processing/schema_utils.py +0 -56
- package/runtime/src/inference/run_kg_grounded_inference.py +0 -370
- package/runtime/src/retrieval/build_faiss_index.py +0 -328
|
@@ -11,16 +11,130 @@ import torch
|
|
|
11
11
|
import yaml
|
|
12
12
|
|
|
13
13
|
try:
|
|
14
|
-
from peft import PeftModel
|
|
15
14
|
from transformers import AutoModelForCausalLM, AutoTokenizer, TextIteratorStreamer
|
|
16
15
|
except ImportError as exc:
|
|
17
16
|
raise ImportError(
|
|
18
|
-
"Student inference requires transformers
|
|
19
|
-
"Install the
|
|
17
|
+
"Student inference requires transformers and torch. "
|
|
18
|
+
"Install the inference dependencies first."
|
|
20
19
|
) from exc
|
|
21
20
|
|
|
21
|
+
from data_processing.utils import resolve_under_root
|
|
22
22
|
from inference.query_router import QueryRouter
|
|
23
|
-
from
|
|
23
|
+
from retrieval.retrieve_evidence import EvidenceRetriever
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
class StudentRAGContext:
|
|
27
|
+
"""Lightweight RAG helper for student-model inference.
|
|
28
|
+
|
|
29
|
+
This intentionally avoids the legacy teacher/LLM inference path; it only
|
|
30
|
+
loads retrieval indexes and formats retrieved context for Willlzh/COFOS.
|
|
31
|
+
"""
|
|
32
|
+
|
|
33
|
+
def __init__(self, config: Dict[str, Any]):
|
|
34
|
+
self.retriever = EvidenceRetriever(config)
|
|
35
|
+
|
|
36
|
+
def format_kg_facts(self, kg_facts: List[Dict[str, Any]], max_items: int = 10) -> str:
|
|
37
|
+
if not kg_facts:
|
|
38
|
+
return "No relevant knowledge graph facts found."
|
|
39
|
+
|
|
40
|
+
support_icon = {
|
|
41
|
+
"supported": "✓",
|
|
42
|
+
"partially_supported": "~",
|
|
43
|
+
"unsupported": "?",
|
|
44
|
+
}
|
|
45
|
+
formatted = []
|
|
46
|
+
for fact in kg_facts[:max_items]:
|
|
47
|
+
head = fact.get("head", "")
|
|
48
|
+
relation = fact.get("relation", "")
|
|
49
|
+
tail = fact.get("tail", "")
|
|
50
|
+
tail_category = fact.get("tail_category", "")
|
|
51
|
+
|
|
52
|
+
line = f"- {head} {relation} {tail}"
|
|
53
|
+
if tail_category:
|
|
54
|
+
line += f" ({tail_category})"
|
|
55
|
+
|
|
56
|
+
condition = fact.get("condition") or {}
|
|
57
|
+
if isinstance(condition, dict):
|
|
58
|
+
condition_text = "; ".join(
|
|
59
|
+
str(value)
|
|
60
|
+
for value in [
|
|
61
|
+
condition.get("phase"),
|
|
62
|
+
condition.get("oxidant_source"),
|
|
63
|
+
condition.get("driving_force"),
|
|
64
|
+
]
|
|
65
|
+
if value
|
|
66
|
+
)
|
|
67
|
+
if condition_text:
|
|
68
|
+
line += f" | condition: {condition_text}"
|
|
69
|
+
|
|
70
|
+
evidence = fact.get("evidence") or {}
|
|
71
|
+
if isinstance(evidence, dict):
|
|
72
|
+
status = evidence.get("support_status") or ""
|
|
73
|
+
level = evidence.get("evidence_level") or ""
|
|
74
|
+
evidence_text = " ".join(filter(None, [status, level]))
|
|
75
|
+
icon = support_icon.get(status, "")
|
|
76
|
+
if icon:
|
|
77
|
+
evidence_text = f"{icon} {evidence_text}"
|
|
78
|
+
else:
|
|
79
|
+
evidence_text = str(evidence)
|
|
80
|
+
if evidence_text:
|
|
81
|
+
line += f" | {evidence_text}"
|
|
82
|
+
|
|
83
|
+
formatted.append(line)
|
|
84
|
+
|
|
85
|
+
return "\n".join(formatted)
|
|
86
|
+
|
|
87
|
+
@staticmethod
|
|
88
|
+
def _truncate_at_sentence(text: str, max_chars: int = 500) -> str:
|
|
89
|
+
if len(text) <= max_chars:
|
|
90
|
+
return text
|
|
91
|
+
snippet = text[:max_chars]
|
|
92
|
+
for sep in (". ", ".\n", "? ", "?\n", "! ", "!\n", "。", ";"):
|
|
93
|
+
idx = snippet.rfind(sep)
|
|
94
|
+
if idx > max_chars * 0.5:
|
|
95
|
+
return snippet[:idx + len(sep.rstrip())]
|
|
96
|
+
last_space = snippet.rfind(" ")
|
|
97
|
+
if last_space > max_chars * 0.5:
|
|
98
|
+
return snippet[:last_space]
|
|
99
|
+
return snippet
|
|
100
|
+
|
|
101
|
+
def format_evidence(self, evidence_list: List[Dict[str, Any]], max_items: int = 10) -> str:
|
|
102
|
+
if not evidence_list:
|
|
103
|
+
return "No relevant evidence found."
|
|
104
|
+
|
|
105
|
+
level_badge = {
|
|
106
|
+
"high": "[HIGH confidence]",
|
|
107
|
+
"medium": "[MEDIUM confidence]",
|
|
108
|
+
"low": "[LOW confidence]",
|
|
109
|
+
}
|
|
110
|
+
support_badge = {
|
|
111
|
+
"supported": "verified",
|
|
112
|
+
"partially_supported": "partial",
|
|
113
|
+
"unsupported": "unverified",
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
formatted = []
|
|
117
|
+
for i, evidence in enumerate(evidence_list[:max_items], 1):
|
|
118
|
+
source_type = evidence.get("source_type", "unknown")
|
|
119
|
+
material = evidence.get("material_context", {}).get("material_type", "N/A")
|
|
120
|
+
dominant = [
|
|
121
|
+
item.get("species", "N/A")
|
|
122
|
+
for item in evidence.get("dominant_ros_or_product", [])
|
|
123
|
+
]
|
|
124
|
+
evidence_level = evidence.get("evidence_level", "low")
|
|
125
|
+
support_status = evidence.get("support_status", "unsupported")
|
|
126
|
+
|
|
127
|
+
level_str = level_badge.get(evidence_level, evidence_level)
|
|
128
|
+
support_str = support_badge.get(support_status, support_status)
|
|
129
|
+
text = self._truncate_at_sentence(evidence.get("text", ""), max_chars=500)
|
|
130
|
+
|
|
131
|
+
formatted.append(
|
|
132
|
+
f"[{i}] {source_type} {level_str} ({support_str})\n"
|
|
133
|
+
f" Material: {material}\n"
|
|
134
|
+
f" Dominant ROS: {', '.join(dominant)}\n"
|
|
135
|
+
f" {text}"
|
|
136
|
+
)
|
|
137
|
+
return "\n\n".join(formatted)
|
|
24
138
|
|
|
25
139
|
|
|
26
140
|
class StudentAdapterInference:
|
|
@@ -48,7 +162,12 @@ class StudentAdapterInference:
|
|
|
48
162
|
self.config = config
|
|
49
163
|
self.model_path_str = model_path
|
|
50
164
|
self.model_path = Path(model_path)
|
|
51
|
-
|
|
165
|
+
if adapter_path:
|
|
166
|
+
raise ValueError(
|
|
167
|
+
"The standalone fwdemo runtime only supports merged models; "
|
|
168
|
+
"publish or pass a merged Transformers checkpoint instead of a LoRA adapter."
|
|
169
|
+
)
|
|
170
|
+
self.adapter_path = None
|
|
52
171
|
self.model_is_hf = self._is_hf_repo(model_path)
|
|
53
172
|
self.use_rag = use_rag
|
|
54
173
|
generation_config = config.get("generation", {})
|
|
@@ -64,6 +183,7 @@ class StudentAdapterInference:
|
|
|
64
183
|
self.min_new_tokens = int(generation_config.get("min_new_tokens", 0))
|
|
65
184
|
self.history_max_turns = int(generation_config.get("history_max_turns", 4))
|
|
66
185
|
self.refine_answers = bool(generation_config.get("refine_answers", True))
|
|
186
|
+
self.stream_refinement = bool(generation_config.get("stream_refinement", True))
|
|
67
187
|
self.refinement_max_new_tokens = int(
|
|
68
188
|
generation_config.get(
|
|
69
189
|
"refinement_max_new_tokens",
|
|
@@ -71,12 +191,12 @@ class StudentAdapterInference:
|
|
|
71
191
|
)
|
|
72
192
|
)
|
|
73
193
|
self.prompt_templates = self._load_prompt_templates()
|
|
74
|
-
self.rag =
|
|
194
|
+
self.rag = StudentRAGContext(config) if use_rag else None
|
|
75
195
|
self.tokenizer = None
|
|
76
196
|
self.model = None
|
|
77
197
|
|
|
78
198
|
def _load_prompt_templates(self) -> Dict[str, str]:
|
|
79
|
-
prompt_path =
|
|
199
|
+
prompt_path = resolve_under_root(
|
|
80
200
|
self.config.get("prompts", {}).get(
|
|
81
201
|
"path", "config/prompt_templates.yaml"
|
|
82
202
|
)
|
|
@@ -101,18 +221,21 @@ class StudentAdapterInference:
|
|
|
101
221
|
return
|
|
102
222
|
if not self.model_path.exists():
|
|
103
223
|
raise FileNotFoundError(f"Base model path not found: {self.model_path}")
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
if
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
224
|
+
|
|
225
|
+
@staticmethod
|
|
226
|
+
def _select_dtype() -> torch.dtype:
|
|
227
|
+
"""Pick the fastest dtype the current device actually supports.
|
|
228
|
+
|
|
229
|
+
bfloat16 was hardcoded, which silently falls back to a very slow
|
|
230
|
+
emulated path on CPU and on GPUs without bf16 support.
|
|
231
|
+
"""
|
|
232
|
+
if torch.cuda.is_available():
|
|
233
|
+
if torch.cuda.is_bf16_supported():
|
|
234
|
+
return torch.bfloat16
|
|
235
|
+
return torch.float16
|
|
236
|
+
if torch.backends.mps.is_available():
|
|
237
|
+
return torch.float16
|
|
238
|
+
return torch.float32
|
|
116
239
|
|
|
117
240
|
def load_model(self) -> None:
|
|
118
241
|
self.validate_paths()
|
|
@@ -135,19 +258,21 @@ class StudentAdapterInference:
|
|
|
135
258
|
)
|
|
136
259
|
if self.tokenizer.pad_token is None:
|
|
137
260
|
self.tokenizer.pad_token = self.tokenizer.eos_token
|
|
261
|
+
# Decoder-only models must be left-padded for correct generation.
|
|
262
|
+
self.tokenizer.padding_side = "left"
|
|
138
263
|
|
|
264
|
+
dtype = self._select_dtype()
|
|
265
|
+
logging.info("Loading model weights with dtype=%s", dtype)
|
|
139
266
|
model = AutoModelForCausalLM.from_pretrained(
|
|
140
267
|
self.model_path_str,
|
|
141
268
|
device_map="auto",
|
|
142
|
-
torch_dtype=
|
|
269
|
+
torch_dtype=dtype,
|
|
270
|
+
low_cpu_mem_usage=True,
|
|
143
271
|
trust_remote_code=True,
|
|
144
272
|
)
|
|
145
|
-
|
|
146
|
-
logging.info("Loading LoRA adapter from %s", self.adapter_path)
|
|
147
|
-
model = PeftModel.from_pretrained(model, str(self.adapter_path))
|
|
148
|
-
else:
|
|
149
|
-
logging.info("Using merged model without a separate LoRA adapter")
|
|
273
|
+
logging.info("Using merged model without a separate LoRA adapter")
|
|
150
274
|
|
|
275
|
+
model.generation_config.use_cache = True
|
|
151
276
|
self.model = model
|
|
152
277
|
self.model.eval()
|
|
153
278
|
|
|
@@ -290,7 +415,7 @@ class StudentAdapterInference:
|
|
|
290
415
|
) -> str:
|
|
291
416
|
prompt = self._render_prompt(messages)
|
|
292
417
|
inputs = self.tokenizer(prompt, return_tensors="pt").to(self.model.device)
|
|
293
|
-
with torch.
|
|
418
|
+
with torch.inference_mode():
|
|
294
419
|
output = self.model.generate(
|
|
295
420
|
**inputs,
|
|
296
421
|
**self._generation_kwargs(max_new_tokens=max_new_tokens),
|
|
@@ -298,6 +423,104 @@ class StudentAdapterInference:
|
|
|
298
423
|
generated = output[0][inputs["input_ids"].shape[-1] :]
|
|
299
424
|
return self.tokenizer.decode(generated, skip_special_tokens=True)
|
|
300
425
|
|
|
426
|
+
def _stream_from_messages(
|
|
427
|
+
self,
|
|
428
|
+
messages: List[Dict[str, str]],
|
|
429
|
+
max_new_tokens: Optional[int] = None,
|
|
430
|
+
) -> Generator[str, None, None]:
|
|
431
|
+
"""Yield generated text incrementally, suppressing reasoning scaffold.
|
|
432
|
+
|
|
433
|
+
Raw token streaming would leak ``<think>`` blocks and leading
|
|
434
|
+
``Assistant:`` / ``Final Answer:`` labels to the user, because
|
|
435
|
+
:meth:`clean_answer` can only run on a complete string. This wraps the
|
|
436
|
+
stream in a small state machine that withholds text until it is known to
|
|
437
|
+
be part of the real answer.
|
|
438
|
+
"""
|
|
439
|
+
prompt = self._render_prompt(messages)
|
|
440
|
+
inputs = self.tokenizer(prompt, return_tensors="pt").to(self.model.device)
|
|
441
|
+
|
|
442
|
+
streamer = TextIteratorStreamer(
|
|
443
|
+
self.tokenizer,
|
|
444
|
+
skip_prompt=True,
|
|
445
|
+
skip_special_tokens=True,
|
|
446
|
+
)
|
|
447
|
+
gen_kwargs = {
|
|
448
|
+
**inputs,
|
|
449
|
+
"streamer": streamer,
|
|
450
|
+
**self._generation_kwargs(max_new_tokens=max_new_tokens),
|
|
451
|
+
}
|
|
452
|
+
|
|
453
|
+
errors: List[BaseException] = []
|
|
454
|
+
|
|
455
|
+
def _run() -> None:
|
|
456
|
+
try:
|
|
457
|
+
with torch.inference_mode():
|
|
458
|
+
self.model.generate(**gen_kwargs)
|
|
459
|
+
except BaseException as exc: # surfaced to the caller below
|
|
460
|
+
errors.append(exc)
|
|
461
|
+
streamer.end()
|
|
462
|
+
|
|
463
|
+
thread = Thread(target=_run, daemon=True)
|
|
464
|
+
thread.start()
|
|
465
|
+
try:
|
|
466
|
+
yield from self._sanitize_stream(streamer)
|
|
467
|
+
finally:
|
|
468
|
+
thread.join()
|
|
469
|
+
if errors:
|
|
470
|
+
raise errors[0]
|
|
471
|
+
|
|
472
|
+
@staticmethod
|
|
473
|
+
def _sanitize_stream(chunks: Any) -> Generator[str, None, None]:
|
|
474
|
+
"""Drop ``<think>`` blocks and leading labels from a text stream."""
|
|
475
|
+
buffer = ""
|
|
476
|
+
started = False
|
|
477
|
+
in_think = False
|
|
478
|
+
|
|
479
|
+
for chunk in chunks:
|
|
480
|
+
buffer += chunk
|
|
481
|
+
|
|
482
|
+
while True:
|
|
483
|
+
if in_think:
|
|
484
|
+
end = buffer.find("</think>")
|
|
485
|
+
if end == -1:
|
|
486
|
+
# Keep only enough tail to match a split closing tag.
|
|
487
|
+
buffer = buffer[-len("</think>") :]
|
|
488
|
+
break
|
|
489
|
+
buffer = buffer[end + len("</think>") :]
|
|
490
|
+
in_think = False
|
|
491
|
+
continue
|
|
492
|
+
|
|
493
|
+
start = buffer.find("<think>")
|
|
494
|
+
if start != -1:
|
|
495
|
+
buffer = buffer[start + len("<think>") :]
|
|
496
|
+
in_think = True
|
|
497
|
+
continue
|
|
498
|
+
break
|
|
499
|
+
|
|
500
|
+
if in_think:
|
|
501
|
+
continue
|
|
502
|
+
|
|
503
|
+
if not started:
|
|
504
|
+
# Hold back until we are past any leading label, and keep a tail
|
|
505
|
+
# that could still turn out to be the start of a "<think>" tag.
|
|
506
|
+
stripped = re.sub(
|
|
507
|
+
r"(?is)^\s*(?:assistant|model|模型|final\s+answer|answer)\s*[::]\s*",
|
|
508
|
+
"",
|
|
509
|
+
buffer,
|
|
510
|
+
)
|
|
511
|
+
if not stripped.strip():
|
|
512
|
+
continue
|
|
513
|
+
buffer = stripped.lstrip()
|
|
514
|
+
started = True
|
|
515
|
+
|
|
516
|
+
if len(buffer) > len("<think>"):
|
|
517
|
+
emit, buffer = buffer[: -len("<think>")], buffer[-len("<think>") :]
|
|
518
|
+
if emit:
|
|
519
|
+
yield emit
|
|
520
|
+
|
|
521
|
+
if not in_think and buffer.strip():
|
|
522
|
+
yield buffer if started else buffer.lstrip()
|
|
523
|
+
|
|
301
524
|
@staticmethod
|
|
302
525
|
def _has_report_style(answer: str) -> bool:
|
|
303
526
|
report_patterns = (
|
|
@@ -316,26 +539,18 @@ class StudentAdapterInference:
|
|
|
316
539
|
draft_answer: str,
|
|
317
540
|
extra_instruction: str = "",
|
|
318
541
|
) -> str:
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
)
|
|
322
|
-
user_prompt_template = self.prompt_templates.get(
|
|
323
|
-
"student_answer_refinement_user_prompt"
|
|
324
|
-
)
|
|
325
|
-
if not system_prompt or not user_prompt_template:
|
|
542
|
+
messages = self._refinement_messages(question, draft_answer)
|
|
543
|
+
if messages is None:
|
|
326
544
|
return draft_answer
|
|
327
545
|
|
|
328
|
-
user_prompt = user_prompt_template.format(
|
|
329
|
-
question=question,
|
|
330
|
-
draft_answer=draft_answer,
|
|
331
|
-
).strip()
|
|
332
546
|
if extra_instruction:
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
547
|
+
messages = [
|
|
548
|
+
messages[0],
|
|
549
|
+
{
|
|
550
|
+
"role": "user",
|
|
551
|
+
"content": f"{messages[1]['content']}\n\n{extra_instruction.strip()}",
|
|
552
|
+
},
|
|
553
|
+
]
|
|
339
554
|
return self.clean_answer(
|
|
340
555
|
self._generate_from_messages(
|
|
341
556
|
messages,
|
|
@@ -399,13 +614,44 @@ class StudentAdapterInference:
|
|
|
399
614
|
def answer(self, question: str, top_k: int = 5) -> str:
|
|
400
615
|
return self.answer_with_metadata(question, top_k=top_k)["answer"]
|
|
401
616
|
|
|
617
|
+
def _refinement_messages(
|
|
618
|
+
self,
|
|
619
|
+
question: str,
|
|
620
|
+
draft_answer: str,
|
|
621
|
+
) -> Optional[List[Dict[str, str]]]:
|
|
622
|
+
system_prompt = self.prompt_templates.get(
|
|
623
|
+
"student_answer_refinement_system_prompt"
|
|
624
|
+
)
|
|
625
|
+
user_prompt_template = self.prompt_templates.get(
|
|
626
|
+
"student_answer_refinement_user_prompt"
|
|
627
|
+
)
|
|
628
|
+
if not system_prompt or not user_prompt_template:
|
|
629
|
+
return None
|
|
630
|
+
return [
|
|
631
|
+
{"role": "system", "content": system_prompt.strip()},
|
|
632
|
+
{
|
|
633
|
+
"role": "user",
|
|
634
|
+
"content": user_prompt_template.format(
|
|
635
|
+
question=question,
|
|
636
|
+
draft_answer=draft_answer,
|
|
637
|
+
).strip(),
|
|
638
|
+
},
|
|
639
|
+
]
|
|
640
|
+
|
|
402
641
|
def answer_stream(
|
|
403
642
|
self,
|
|
404
643
|
question: str,
|
|
405
644
|
top_k: int = 5,
|
|
406
645
|
history: Optional[List[Dict[str, str]]] = None,
|
|
407
646
|
) -> Generator[str, None, None]:
|
|
408
|
-
"""Stream
|
|
647
|
+
"""Stream the answer for *question* incrementally.
|
|
648
|
+
|
|
649
|
+
With ``refine_answers`` on, the draft pass is generated silently and the
|
|
650
|
+
*refinement* pass is the one streamed, so the user sees the final text
|
|
651
|
+
appear token by token rather than waiting for two full generations. Set
|
|
652
|
+
``generation.stream_refinement: false`` to keep the stricter
|
|
653
|
+
non-streaming path, which can additionally retry a report-style answer.
|
|
654
|
+
"""
|
|
409
655
|
quick_response = QueryRouter.quick_response(question)
|
|
410
656
|
if quick_response is not None:
|
|
411
657
|
yield quick_response
|
|
@@ -414,7 +660,7 @@ class StudentAdapterInference:
|
|
|
414
660
|
if self.model is None or self.tokenizer is None:
|
|
415
661
|
self.load_model()
|
|
416
662
|
|
|
417
|
-
if self.refine_answers:
|
|
663
|
+
if self.refine_answers and not self.stream_refinement:
|
|
418
664
|
yield self.answer_with_metadata(
|
|
419
665
|
question,
|
|
420
666
|
top_k=top_k,
|
|
@@ -423,24 +669,29 @@ class StudentAdapterInference:
|
|
|
423
669
|
return
|
|
424
670
|
|
|
425
671
|
context = self._rag_context(question, top_k=top_k)
|
|
426
|
-
|
|
427
|
-
inputs = self.tokenizer(prompt, return_tensors="pt").to(self.model.device)
|
|
672
|
+
messages = self._with_history(context["messages"], history)
|
|
428
673
|
|
|
429
|
-
|
|
430
|
-
self.
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
)
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
}
|
|
439
|
-
thread = Thread(target=self.model.generate, kwargs=gen_kwargs)
|
|
440
|
-
thread.start()
|
|
674
|
+
if not self.refine_answers:
|
|
675
|
+
yield from self._stream_from_messages(messages)
|
|
676
|
+
return
|
|
677
|
+
|
|
678
|
+
draft = self.clean_answer(self._generate_from_messages(messages))
|
|
679
|
+
refinement = self._refinement_messages(question, draft)
|
|
680
|
+
if refinement is None:
|
|
681
|
+
yield draft
|
|
682
|
+
return
|
|
441
683
|
|
|
442
|
-
|
|
443
|
-
|
|
684
|
+
streamed = ""
|
|
685
|
+
for chunk in self._stream_from_messages(
|
|
686
|
+
refinement,
|
|
687
|
+
max_new_tokens=self.refinement_max_new_tokens,
|
|
688
|
+
):
|
|
689
|
+
streamed += chunk
|
|
690
|
+
yield chunk
|
|
691
|
+
|
|
692
|
+
if not streamed.strip():
|
|
693
|
+
# The refinement pass produced nothing usable; fall back to the draft.
|
|
694
|
+
yield draft
|
|
444
695
|
|
|
445
696
|
@staticmethod
|
|
446
697
|
def _strip_reasoning_scaffold(answer: str) -> str:
|
|
@@ -1,10 +1,17 @@
|
|
|
1
1
|
#!/usr/bin/env python3
|
|
2
|
-
"""
|
|
3
|
-
COF-ROS-Reasoner: 检索模块初始化
|
|
4
|
-
"""
|
|
2
|
+
"""COFOS retrieval package."""
|
|
5
3
|
|
|
6
|
-
from .build_faiss_index import FAISSIndexBuilder
|
|
7
|
-
from .build_bm25_index import BM25IndexBuilder
|
|
8
|
-
from .retrieve_evidence import EvidenceRetriever
|
|
9
4
|
|
|
10
|
-
|
|
5
|
+
def __getattr__(name):
|
|
6
|
+
if name == "BM25IndexBuilder":
|
|
7
|
+
from .build_bm25_index import BM25IndexBuilder
|
|
8
|
+
|
|
9
|
+
return BM25IndexBuilder
|
|
10
|
+
if name == "EvidenceRetriever":
|
|
11
|
+
from .retrieve_evidence import EvidenceRetriever
|
|
12
|
+
|
|
13
|
+
return EvidenceRetriever
|
|
14
|
+
raise AttributeError(f"module {__name__!r} has no attribute {name!r}")
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
__all__ = ["BM25IndexBuilder", "EvidenceRetriever"]
|