|
|
|
|
|
|
|
|
import re
|
|
|
import torch
|
|
|
import logging
|
|
|
from transformers import (
|
|
|
PreTrainedModel,
|
|
|
AutoTokenizer,
|
|
|
GenerationConfig,
|
|
|
GenerationMixin,
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
AutoProcessor,
|
|
|
AutoModel,
|
|
|
AutoConfig,
|
|
|
|
|
|
|
|
|
|
|
|
)
|
|
|
from transformers.utils import is_accelerate_available, is_bitsandbytes_available
|
|
|
from typing import Optional, List, Tuple, Dict, Union, Any
|
|
|
import gc
|
|
|
import time
|
|
|
from collections import Counter
|
|
|
from PIL import Image
|
|
|
import io
|
|
|
import os
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
try:
|
|
|
from Enhanced_MemoryEngine import MemoryEngine
|
|
|
from NeuroMemoryProcessor import NeuroMemoryProcessor
|
|
|
from AGIEnhancer import AGIEnhancer
|
|
|
from FullAGI_ExpansionModule import NeoSentientCore
|
|
|
|
|
|
from SimulatedSelfAssessment import SimulatedSelfAssessment
|
|
|
|
|
|
AGI_IMPORTS_SUCCESS = True
|
|
|
logger = logging.getLogger(__name__)
|
|
|
logger.info("AGI helper modules imported successfully.")
|
|
|
except ImportError as e:
|
|
|
AGI_IMPORTS_SUCCESS = False
|
|
|
logger = logging.getLogger(__name__)
|
|
|
logger.error(f"Failed to import AGI helper modules. AGI features will be disabled: {e}")
|
|
|
|
|
|
class MemoryEngine:
|
|
|
def __init__(self, *args, **kwargs): pass
|
|
|
def __getattr__(self, name): return lambda *args, **kwargs: None
|
|
|
class NeuroMemoryProcessor:
|
|
|
def __init__(self, *args, **kwargs): pass
|
|
|
def __getattr__(self, name): return lambda *args, **kwargs: None
|
|
|
class AGIEnhancer:
|
|
|
def __init__(self, *args, **kwargs): pass
|
|
|
def __getattr__(self, name): return lambda *args, **kwargs: None
|
|
|
class NeoSentientCore:
|
|
|
def __init__(self, *args, **kwargs): pass
|
|
|
def __getattr__(self, name): return lambda *args, **kwargs: None
|
|
|
|
|
|
class SimulatedSelfAssessment:
|
|
|
def __init__(self, *args, **kwargs): pass
|
|
|
def __getattr__(self, name): return lambda *args, **kwargs: {"state_summary": "Simulated self-assessment module not available."}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
if not logging.root.handlers:
|
|
|
logging.basicConfig(level=logging.DEBUG, format='%(asctime)s - %(name)s - %(levelname)s - %(message)s')
|
|
|
|
|
|
logger = logging.getLogger(__name__)
|
|
|
if not logger.handlers:
|
|
|
handler = logging.StreamHandler()
|
|
|
formatter = logging.Formatter('%(asctime)s - %(name)s - %(levelname)s - %(message)s')
|
|
|
handler.setFormatter(formatter)
|
|
|
logger.addHandler(handler)
|
|
|
logger.propagate = False
|
|
|
logger.setLevel(logging.DEBUG)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
DEFAULT_MAX_LENGTH = 2048
|
|
|
DEFAULT_REASONING_LIMIT = 15
|
|
|
DEFAULT_CONSISTENCY_ROUNDS = 5
|
|
|
|
|
|
DEFAULT_FINAL_ANSWER_TAG = "Final Answer:"
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
DEFAULT_STEP_PATTERN = re.compile(
|
|
|
r"^(?:Step\s*\d+[:.)-]\s*|\d+[:.)-]\s*)(.*)", re.IGNORECASE
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
ARTIFACT_PATTERNS = [
|
|
|
re.compile(r"<init>.*?</init>", re.DOTALL),
|
|
|
re.compile(r"<final_output>.*?</final_output>", re.DOTALL),
|
|
|
|
|
|
|
|
|
]
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def normalize_answer(answer: str) -> str:
|
|
|
"""
|
|
|
Normalizes a string answer for robust comparison during voting.
|
|
|
- Converts to lowercase.
|
|
|
- Strips leading/trailing whitespace.
|
|
|
- Removes common punctuation and articles.
|
|
|
- Handles simple cases of number words (e.g., "two" -> "2").
|
|
|
- Removes extra internal whitespace.
|
|
|
"""
|
|
|
if not isinstance(answer, str):
|
|
|
return ""
|
|
|
|
|
|
normalized = answer.lower().strip()
|
|
|
|
|
|
|
|
|
normalized = re.sub(r'[.,!?;:]+$', '', normalized).strip()
|
|
|
|
|
|
|
|
|
normalized = re.sub(r'^\s*(?:the answer is|result|output)\s*[:\-]?\s*', '', normalized, flags=re.IGNORECASE).strip()
|
|
|
|
|
|
|
|
|
normalized = re.sub(r'^\s*(a|an|the)\s+', '', normalized, flags=re.IGNORECASE).strip()
|
|
|
|
|
|
|
|
|
num_word_map = {
|
|
|
'zero': '0', 'one': '1', 'two': '2', 'three': '3', 'four': '4',
|
|
|
'five': '5', 'six': '6', 'seven': '7', 'eight': '8', 'nine': '9',
|
|
|
'ten': '10', 'eleven': '11', 'twelve': '12', 'thirteen': '13',
|
|
|
'fourteen': '14', 'fifteen': '15', 'sixteen': '16', 'seventeen': '17',
|
|
|
'eighteen': '18', 'nineteen': '19', 'twenty': '20', 'thirty': '30',
|
|
|
'forty': '40', 'fifty': '50', 'sixty': '60', 'seventy': '70',
|
|
|
'eighty': '80', 'ninety': '90', 'hundred': '100', 'thousand': '1000',
|
|
|
'million': '1000000', 'billion': '1000000000'
|
|
|
}
|
|
|
|
|
|
|
|
|
words = normalized.split()
|
|
|
normalized_words = [num_word_map.get(word, word) for word in words]
|
|
|
normalized = " ".join(normalized_words)
|
|
|
|
|
|
|
|
|
|
|
|
normalized = re.sub(r'\s+', ' ', normalized).strip()
|
|
|
|
|
|
|
|
|
normalized = normalized.strip()
|
|
|
|
|
|
|
|
|
return normalized
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
class ChainOfThoughtWrapper:
|
|
|
"""
|
|
|
ChainOfThoughtWrapper: Orchestrates model generation with CoT prompting
|
|
|
and interacts with AGI helper modules.
|
|
|
|
|
|
Supports multimodal input (image + text) for compatible models
|
|
|
loaded with Hugging Face's AutoModel and AutoProcessor.
|
|
|
"""
|
|
|
def __init__(
|
|
|
self,
|
|
|
model: Union[PreTrainedModel, GenerationMixin, AutoModel, Any],
|
|
|
processor: Union[AutoTokenizer, AutoProcessor, Any],
|
|
|
device: Union[str, torch.device],
|
|
|
|
|
|
|
|
|
cot_instruction: str = "Analyze this step by step to find the answer.",
|
|
|
reasoning_header: str = "Reasoning:",
|
|
|
step_prefix: str = "Step",
|
|
|
final_answer_tag: str = DEFAULT_FINAL_ANSWER_TAG,
|
|
|
max_length: int = DEFAULT_MAX_LENGTH
|
|
|
):
|
|
|
"""
|
|
|
Initializes the ChainOfThoughtWrapper.
|
|
|
|
|
|
Args:
|
|
|
model (Union[PreTrainedModel, GenerationMixin, AutoModel, Any]): The loaded Hugging Face model.
|
|
|
processor (Union[AutoTokenizer, AutoProcessor, Any]): The loaded Hugging Face processor
|
|
|
(tokenizer or multimodal processor).
|
|
|
device (Union[str, torch.device]): The device the model is on.
|
|
|
cot_instruction (str): The core instruction phrase for CoT.
|
|
|
reasoning_header (str): The header text before the reasoning steps.
|
|
|
step_prefix (str): The prefix for the first step.
|
|
|
final_answer_tag (str): The specific string marker expected before the final answer.
|
|
|
max_length (int): The maximum combined length of input prompt and generated tokens.
|
|
|
"""
|
|
|
logger.debug("ChainOfThoughtWrapper __init__ started.")
|
|
|
self.model = model
|
|
|
self.processor = processor
|
|
|
self.device = device
|
|
|
self.cot_instruction = cot_instruction
|
|
|
self.reasoning_header = reasoning_header
|
|
|
self.step_prefix = step_prefix
|
|
|
self.final_answer_tag = final_answer_tag
|
|
|
self.max_length = max_length
|
|
|
self._artifact_patterns = ARTIFACT_PATTERNS
|
|
|
self.reasoning_steps_limit = DEFAULT_REASONING_LIMIT
|
|
|
|
|
|
|
|
|
|
|
|
self.multimodal_capable = hasattr(self.processor, 'image_processor') and self.processor.image_processor is not None
|
|
|
logger.info(f"Wrapper initialized on {self.device}. Multimodal capability detected: {self.multimodal_capable}")
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
self.tokenizer = getattr(self.processor, 'tokenizer', self.processor)
|
|
|
|
|
|
if self.tokenizer is None:
|
|
|
logger.error("Processor does not contain a tokenizer.")
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
if self.tokenizer and self.tokenizer.pad_token_id is None:
|
|
|
if hasattr(self.tokenizer, 'eos_token_id') and self.tokenizer.eos_token_id is not None:
|
|
|
self.tokenizer.pad_token_id = self.tokenizer.eos_token_id
|
|
|
logger.warning("Tokenizer pad_token_id is None, using eos_token_id (%s) as pad_token_id for batching.", self.tokenizer.eos_token_id)
|
|
|
else:
|
|
|
|
|
|
logger.warning("Tokenizer pad_token_id and eos_token_id are both None. Attempting to add a [PAD] token.")
|
|
|
try:
|
|
|
|
|
|
if hasattr(self.tokenizer, 'vocab') and '[PAD]' not in self.tokenizer.vocab:
|
|
|
self.tokenizer.add_special_tokens({'pad_token': '[PAD]'})
|
|
|
|
|
|
|
|
|
logger.warning("Added new [PAD] token to tokenizer. Model embeddings may need resizing.")
|
|
|
elif not hasattr(self.tokenizer, 'vocab'):
|
|
|
logger.warning("Tokenizer does not have a vocabulary attribute. Cannot check for or add [PAD] token.")
|
|
|
else:
|
|
|
logger.info("[PAD] token already exists in tokenizer vocabulary.")
|
|
|
|
|
|
|
|
|
if self.tokenizer.pad_token_id is None and hasattr(self.tokenizer, 'convert_tokens_to_ids'):
|
|
|
self.tokenizer.pad_token_id = self.tokenizer.convert_tokens_to_ids('[PAD]')
|
|
|
logger.info("Set pad_token_id to ID of [PAD] token (%s).", self.tokenizer.pad_token_id)
|
|
|
elif self.tokenizer.pad_token_id is None:
|
|
|
logger.warning("Cannot set pad_token_id as convert_tokens_to_ids method is missing.")
|
|
|
|
|
|
|
|
|
except Exception as e:
|
|
|
logger.error(f"Failed to add [PAD] token or set pad_token_id: {e}")
|
|
|
self.tokenizer.pad_token_id = None
|
|
|
logger.warning("Failed to set pad_token_id. Batch generation might fail.")
|
|
|
elif self.tokenizer:
|
|
|
logger.debug("Tokenizer has pad_token_id: %s", self.tokenizer.pad_token_id)
|
|
|
else:
|
|
|
logger.warning("No tokenizer available to check or set pad_token_id.")
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
self.final_answer_pattern = re.compile(
|
|
|
re.escape(final_answer_tag) + r"\s*(.*)", re.IGNORECASE | re.DOTALL
|
|
|
)
|
|
|
self._step_pattern = DEFAULT_STEP_PATTERN
|
|
|
|
|
|
logger.debug("Final answer pattern compiled: %s", self.final_answer_pattern.pattern)
|
|
|
logger.debug("Step pattern: %s", self._step_pattern.pattern)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
self.memory_engine = None
|
|
|
self.neuro_processor = None
|
|
|
self.agi_enhancer = None
|
|
|
self.neo_sentient_core = None
|
|
|
|
|
|
self.self_assessment_module = None
|
|
|
|
|
|
if AGI_IMPORTS_SUCCESS:
|
|
|
try:
|
|
|
self.memory_engine = MemoryEngine()
|
|
|
logger.info("MemoryEngine initialized.")
|
|
|
except Exception as e:
|
|
|
self.memory_engine = None
|
|
|
logger.error(f"Failed to initialize MemoryEngine: {e}")
|
|
|
|
|
|
try:
|
|
|
self.neuro_processor = NeuroMemoryProcessor()
|
|
|
logger.info("NeuroMemoryProcessor initialized.")
|
|
|
except Exception as e:
|
|
|
self.neuro_processor = None
|
|
|
logger.error(f"Failed to initialize NeuroMemoryProcessor: {e}")
|
|
|
|
|
|
try:
|
|
|
self.agi_enhancer = AGIEnhancer()
|
|
|
logger.info("AGIEnhancer initialized.")
|
|
|
except Exception as e:
|
|
|
self.agi_enhancer = None
|
|
|
logger.error(f"Failed to initialize AGIEnhancer: {e}")
|
|
|
|
|
|
try:
|
|
|
self.neo_sentient_core = NeoSentientCore(name="NeoAGI")
|
|
|
logger.info("NeoSentientCore initialized.")
|
|
|
except Exception as e:
|
|
|
self.neo_sentient_core = None
|
|
|
logger.error(f"Failed to initialize NeoSentientCore: {e}")
|
|
|
|
|
|
|
|
|
try:
|
|
|
self.self_assessment_module = SimulatedSelfAssessment()
|
|
|
logger.info("SimulatedSelfAssessment initialized.")
|
|
|
except Exception as e:
|
|
|
self.self_assessment_module = None
|
|
|
logger.error(f"Failed to initialize SimulatedSelfAssessment: {e}")
|
|
|
|
|
|
else:
|
|
|
logger.warning("AGI helper modules were not imported, AGI features will not be available.")
|
|
|
|
|
|
|
|
|
logger.debug("ChainOfThoughtWrapper __init__ finished.")
|
|
|
|
|
|
|
|
|
@torch.no_grad()
|
|
|
def generate(
|
|
|
self,
|
|
|
input_text: str,
|
|
|
image_data: Optional[List[bytes]] = None,
|
|
|
multimodal_model: bool = False,
|
|
|
generation_params: Optional[Dict[str, Any]] = None,
|
|
|
chat_history: Optional[List[Dict[str, str]]] = None
|
|
|
) -> Tuple[Optional[List[Dict[str, str]]], Optional[str], Optional[str]]:
|
|
|
"""
|
|
|
Generates a Chain-of-Thought response from the language model, optionally
|
|
|
handling multimodal input (text + image). Integrates AGI helper modules
|
|
|
(MemoryEngine, NeuroProcessor, AGIEnhancer, NeoSentientCore, SelfAssessment)
|
|
|
and includes conversation history in the prompt.
|
|
|
|
|
|
Args:
|
|
|
prompt (str): The user's input prompt (text part).
|
|
|
image (Optional[Image.Image]): The input image, if any.
|
|
|
multimodal_model (bool): True if the loaded model is multimodal.
|
|
|
generation_params (Optional[Dict[str, Any]]): Dictionary of generation parameters
|
|
|
chat_history (Optional[List[Dict[str, str]]]): A list of dictionaries
|
|
|
representing previous turns of the conversation. Each dict
|
|
|
is expected to have keys 'role' ('user' or 'assistant')
|
|
|
and 'content' (the message text).
|
|
|
|
|
|
Returns:
|
|
|
Tuple[Optional[List[Dict[str, str]]], Optional[str], Optional[str]]:
|
|
|
A tuple containing:
|
|
|
1. List of dictionaries representing the parsed CoT steps (or None).
|
|
|
2. The extracted final answer string (or None).
|
|
|
3. The raw body text of the model's response (or None).
|
|
|
"""
|
|
|
logger.debug("Wrapper generate method called.")
|
|
|
|
|
|
if self.model is None or self.processor is None or self.tokenizer is None or \
|
|
|
not (hasattr(self.model, 'generate') and callable(getattr(self.model, 'generate', None)) or isinstance(self.model, GenerationMixin)):
|
|
|
logger.error("Model, Processor, Tokenizer not loaded or loaded model is not generation compatible.")
|
|
|
|
|
|
return {"full_texts": [], "reasoning_steps": [], "final_answers": [], "generated_images": [], "generation_scores": None}
|
|
|
|
|
|
|
|
|
|
|
|
params = generation_params if generation_params is not None else {}
|
|
|
effective_num_return_sequences = params.get("num_return_sequences", 1)
|
|
|
|
|
|
max_new_tokens = params.get("max_new_tokens", 512)
|
|
|
temperature = params.get("temperature", 0.7)
|
|
|
top_k = params.get("top_k", 50)
|
|
|
top_p = params.get("top_p", 1.0)
|
|
|
do_sample = params.get("do_sample", True)
|
|
|
repetition_penalty = params.get("repetition_penalty", 1.1)
|
|
|
no_repeat_ngram_size = params.get("no_repeat_ngram_size", 0)
|
|
|
|
|
|
|
|
|
logger.info(f"Generating {effective_num_return_sequences} sequence(s) with params: {params}")
|
|
|
if image_data:
|
|
|
logger.info(f"Received {len(image_data)} image(s). Wrapper multimodal capable: {self.multimodal_capable}")
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
agi_pre_prompt_elements: List[str] = []
|
|
|
if AGI_IMPORTS_SUCCESS and self.neo_sentient_core:
|
|
|
|
|
|
perception_detail = f"User input: '{input_text[:200]}{'...' if len(input_text) > 200 else ''}'"
|
|
|
if image_data:
|
|
|
perception_detail += f" (with {len(image_data)} image(s))"
|
|
|
try:
|
|
|
self.neo_sentient_core.perceive(perception_detail)
|
|
|
logger.debug("NeoSentientCore perceived input.")
|
|
|
except Exception as e:
|
|
|
logger.warning(f"NeoSentientCore perceive failed: {e}")
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
try:
|
|
|
current_goal = self.neo_sentient_core.decide_goal()
|
|
|
if current_goal and isinstance(current_goal, str): agi_pre_prompt_elements.append(f"Intention: {current_goal.strip()}")
|
|
|
except Exception as e:
|
|
|
logger.warning(f"NeoSentientCore decide_goal failed: {e}")
|
|
|
|
|
|
|
|
|
try:
|
|
|
inner_monologue = self.neo_sentient_core.inner_voice()
|
|
|
if inner_monologue and isinstance(inner_monologue, str): agi_pre_prompt_elements.append(f"InnerVoice: {inner_monologue.strip()}")
|
|
|
except Exception as e:
|
|
|
logger.warning(f"NeoSentientCore inner_voice failed: {e}")
|
|
|
|
|
|
|
|
|
|
|
|
try:
|
|
|
qualia_token = self.neo_sentient_core.generate_qualia_token("curiosity")
|
|
|
if qualia_token and isinstance(qualia_token, str): agi_pre_prompt_elements.insert(0, qualia_token.strip())
|
|
|
except Exception as e:
|
|
|
logger.warning(f"NeoSentientCore generate_qualia_token failed: {e}")
|
|
|
|
|
|
|
|
|
if AGI_IMPORTS_SUCCESS and self.agi_enhancer:
|
|
|
|
|
|
|
|
|
enhancer_experience_detail = f"User input: '{input_text[:200]}{'...' if len(input_text) > 200 else ''}'"
|
|
|
if image_data:
|
|
|
enhancer_experience_detail += f" (with {len(image_data)} image(s))"
|
|
|
try:
|
|
|
self.agi_enhancer.log_experience(enhancer_experience_detail)
|
|
|
logger.debug("AGIEnhancer logged experience.")
|
|
|
except Exception as e:
|
|
|
logger.warning(f"AGIEnhancer log_experience failed: {e}")
|
|
|
|
|
|
|
|
|
self_assessment_summary_text: Optional[str] = None
|
|
|
if AGI_IMPORTS_SUCCESS and self.self_assessment_module and \
|
|
|
self.memory_engine and self.neuro_processor and self.neo_sentient_core:
|
|
|
try:
|
|
|
|
|
|
|
|
|
recent_reflections_snapshot = self.memory_engine.recall(include_long_term=True, include_working=True, limit=5)
|
|
|
top_biases_snapshot = self.neuro_processor.recall_biases(top_k=10)
|
|
|
synaptic_weights_snapshot = self.neuro_processor.recall_weights(top_k=10)
|
|
|
neo_state_snapshot = self.neo_sentient_core.get_state()
|
|
|
current_emotions_snapshot = neo_state_snapshot.get("emotions", {})
|
|
|
intent_pool_snapshot = neo_state_snapshot.get("intent_pool", [])
|
|
|
|
|
|
|
|
|
qri_snapshot_data = None
|
|
|
|
|
|
|
|
|
assessment_result = self.self_assessment_module.perform_assessment(
|
|
|
recent_reflections=recent_reflections_snapshot,
|
|
|
top_biases=top_biases_snapshot,
|
|
|
synaptic_weights_snapshot=synaptic_weights_snapshot,
|
|
|
current_emotions=current_emotions_snapshot,
|
|
|
intent_pool=intent_pool_snapshot,
|
|
|
|
|
|
trace_summary=self.memory_engine.get_trace()[-10:] if self.memory_engine and len(self.memory_engine.get_trace()) > 0 else [],
|
|
|
qri_snapshot=qri_snapshot_data
|
|
|
)
|
|
|
|
|
|
self_assessment_summary_text = assessment_result.get("state_summary", None)
|
|
|
logger.debug("Performed simulated self-assessment and retrieved summary for prompt.")
|
|
|
except Exception as e:
|
|
|
logger.error(f"Failed to perform simulated self-assessment: {e}")
|
|
|
|
|
|
self_assessment_summary_text = "\n--- Simulated Self-Assessment Error ---\nInternal assessment module encountered an issue and cannot provide a state summary.\n---\n"
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
agi_pre_prompt = "\n".join(agi_pre_prompt_elements) + "\n\n" if agi_pre_prompt_elements else ""
|
|
|
|
|
|
|
|
|
self_assessment_prompt_part = self_assessment_summary_text + "\n\n" if self_assessment_summary_text else ""
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
cot_instruction_text = (
|
|
|
f"{self.cot_instruction}\n\n"
|
|
|
|
|
|
"Based on the provided 'Simulated Internal State Assessment', incorporate insights about your perceived internal state, coherence, and well-being into your response and reasoning process.\n\n"
|
|
|
)
|
|
|
|
|
|
|
|
|
cot_prompt_core_text = (
|
|
|
cot_instruction_text +
|
|
|
f"{self.reasoning_header}\n\n"
|
|
|
f"{self.step_prefix} 1: "
|
|
|
)
|
|
|
|
|
|
|
|
|
history_prompt_part = ""
|
|
|
if chat_history:
|
|
|
logger.debug(f"Including {len(chat_history)} turns in conversation history prompt part.")
|
|
|
formatted_history_lines = []
|
|
|
for turn in chat_history:
|
|
|
role = turn.get('role', 'unknown').capitalize()
|
|
|
|
|
|
raw_content = turn.get('content', '')
|
|
|
if isinstance(raw_content, str):
|
|
|
content = raw_content.strip()
|
|
|
else:
|
|
|
content = str(raw_content).strip()
|
|
|
|
|
|
if role and content:
|
|
|
formatted_history_lines.append(f"{role}: {content}")
|
|
|
|
|
|
history_prompt_part = "\n".join(formatted_history_lines) + "\n\n---\n\n" if formatted_history_lines else ""
|
|
|
logger.debug(f"Formatted history prompt part:\n{history_prompt_part[:500]}...")
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
full_text_prompt = history_prompt_part + agi_pre_prompt + self_assessment_prompt_part + cot_prompt_core_text
|
|
|
|
|
|
|
|
|
|
|
|
input_tensors = {}
|
|
|
|
|
|
try:
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
messages = []
|
|
|
if image_data and self.multimodal_capable:
|
|
|
for img_bytes in image_data:
|
|
|
try:
|
|
|
img = Image.open(io.BytesIO(img_bytes))
|
|
|
messages.append({"type": "image", "content": img})
|
|
|
except Exception as e:
|
|
|
logger.warning(f"Could not open image from bytes for processing: {e}. Skipping this image.")
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
processor_messages = []
|
|
|
|
|
|
if input_text and input_text.strip():
|
|
|
processor_messages.append({"type": "text", "content": f"User Input: {input_text.strip()}"})
|
|
|
|
|
|
|
|
|
if image_data and self.multimodal_capable and messages:
|
|
|
processor_messages.extend(messages)
|
|
|
logger.debug(f"Prepared {len(messages)} image messages for processor.")
|
|
|
elif image_data and not self.multimodal_capable:
|
|
|
logger.warning("Image data provided but wrapper/model is text-only. Images will be ignored by the processor.")
|
|
|
|
|
|
|
|
|
|
|
|
if full_text_prompt.strip():
|
|
|
processor_messages.append({"type": "text", "content": full_text_prompt.strip()})
|
|
|
elif not processor_messages:
|
|
|
logger.warning("No text or image content in messages. Adding a default text message.")
|
|
|
processor_messages.append({"type": "text", "content": "Please provide input."})
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
logger.debug(f"Messages prepared for processor: {processor_messages}")
|
|
|
|
|
|
|
|
|
|
|
|
tokenizer_for_template = getattr(self.processor, 'tokenizer', None)
|
|
|
has_chat_template = tokenizer_for_template and hasattr(tokenizer_for_template, 'apply_chat_template') and tokenizer_for_template.chat_template
|
|
|
|
|
|
if hasattr(self.processor, '__call__') and has_chat_template:
|
|
|
|
|
|
logger.debug("Processor is callable and has a chat template. Using processor's chat template to format messages.")
|
|
|
|
|
|
|
|
|
chat_prompt_text = tokenizer_for_template.apply_chat_template(processor_messages, tokenize=False, add_generation_prompt=True)
|
|
|
logger.debug(f"Chat template applied. Resulting text prompt: {chat_prompt_text[:200]}...")
|
|
|
|
|
|
|
|
|
inputs = self.tokenizer(
|
|
|
chat_prompt_text,
|
|
|
return_tensors="pt",
|
|
|
padding="longest",
|
|
|
truncation=True,
|
|
|
max_length=self.max_length,
|
|
|
).to(self.device)
|
|
|
|
|
|
|
|
|
if image_data and self.multimodal_capable and messages:
|
|
|
image_processor_component = getattr(self.processor, 'image_processor', None)
|
|
|
if image_processor_component:
|
|
|
try:
|
|
|
|
|
|
pil_images = [msg["content"] for msg in messages if msg["type"] == "image" and isinstance(msg["content"], Image.Image)]
|
|
|
if pil_images:
|
|
|
image_inputs = image_processor_component(
|
|
|
pil_images,
|
|
|
return_tensors="pt"
|
|
|
).to(self.device)
|
|
|
|
|
|
inputs.update(image_inputs)
|
|
|
logger.debug(f"Image inputs processed separately and merged for chat template case. Keys now: {inputs.keys()}")
|
|
|
else:
|
|
|
logger.warning("No valid PIL images found in messages despite image_data for chat template case. Skipping image processing.")
|
|
|
|
|
|
except Exception as image_process_e:
|
|
|
logger.error(f"Failed to process image inputs separately for chat template case: {image_process_e}. Generation might fail.")
|
|
|
|
|
|
else:
|
|
|
logger.warning("Processor's image_processor component is missing despite multimodal capability flag for chat template case. Cannot process images.")
|
|
|
|
|
|
|
|
|
elif hasattr(self.processor, '__call__'):
|
|
|
|
|
|
|
|
|
logger.debug("Processor is callable but no chat template. Concatenating text messages and processing images separately.")
|
|
|
|
|
|
|
|
|
concatenated_text_input = "\n".join([msg["content"] for msg in processor_messages if msg["type"] == "text"])
|
|
|
|
|
|
if not concatenated_text_input.strip() and any(msg["type"] == "image" for msg in processor_messages):
|
|
|
|
|
|
|
|
|
logger.warning("No text content in messages, but images are present. Passing empty string as text input.")
|
|
|
concatenated_text_input = ""
|
|
|
elif not concatenated_text_input.strip():
|
|
|
|
|
|
logger.warning("No text or image content in messages. Passing empty string as text input.")
|
|
|
concatenated_text_input = ""
|
|
|
|
|
|
|
|
|
text_input_for_processor = [concatenated_text_input] * effective_num_return_sequences
|
|
|
logger.debug(f"Concatenated text input for processor: '{concatenated_text_input[:200]}...' (duplicated {effective_num_return_sequences} times for batching)")
|
|
|
|
|
|
|
|
|
image_inputs = {}
|
|
|
if image_data and self.multimodal_capable and messages:
|
|
|
image_processor_component = getattr(self.processor, 'image_processor', None)
|
|
|
if image_processor_component:
|
|
|
try:
|
|
|
|
|
|
pil_images = [msg["content"] for msg in messages if msg["type"] == "image" and isinstance(msg["content"], Image.Image)]
|
|
|
if pil_images:
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
image_inputs = image_processor_component(
|
|
|
pil_images,
|
|
|
return_tensors="pt"
|
|
|
).to(self.device)
|
|
|
logger.debug(f"Image inputs processed separately for callable processor without chat template. Keys now: {image_inputs.keys()}")
|
|
|
|
|
|
else:
|
|
|
logger.warning("No valid PIL images found in messages despite image_data for callable processor without chat template. Skipping image processing.")
|
|
|
|
|
|
except Exception as image_process_e:
|
|
|
logger.error(f"Failed to process image inputs separately for callable processor without chat template: {image_process_e}. Generation might fail.")
|
|
|
|
|
|
else:
|
|
|
logger.warning("Processor's image_processor component is missing despite multimodal capability flag for callable processor without chat template. Cannot process images.")
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
inputs = self.processor(
|
|
|
text=text_input_for_processor,
|
|
|
**image_inputs,
|
|
|
return_tensors="pt",
|
|
|
padding="longest",
|
|
|
truncation=True,
|
|
|
max_length=self.max_length,
|
|
|
).to(self.device)
|
|
|
logger.debug("Input processed using processor.__call__ with concatenated text and separate image inputs.")
|
|
|
|
|
|
|
|
|
elif hasattr(self.processor, 'tokenizer'):
|
|
|
|
|
|
logger.debug("Processor is text-only (using tokenizer). Processing text input only.")
|
|
|
|
|
|
|
|
|
|
|
|
combined_text_for_tokenizer = f"User Input: {input_text.strip()}\n\n{full_text_prompt.strip()}"
|
|
|
|
|
|
inputs = self.tokenizer(
|
|
|
combined_text_for_tokenizer,
|
|
|
return_tensors="pt",
|
|
|
padding="longest",
|
|
|
truncation=True,
|
|
|
max_length=self.max_length,
|
|
|
).to(self.device)
|
|
|
logger.debug("Input processed using tokenizer directly.")
|
|
|
|
|
|
|
|
|
else:
|
|
|
|
|
|
raise TypeError("Loaded processor is neither callable nor contains a tokenizer attribute.")
|
|
|
|
|
|
|
|
|
|
|
|
input_tensors = inputs
|
|
|
|
|
|
|
|
|
logger.debug("Input tensors prepared for model.generate. Keys: %s", list(input_tensors.keys()))
|
|
|
if 'input_ids' in input_tensors:
|
|
|
logger.debug("Input IDs shape: %s, dtype: %s, on device: %s", input_tensors['input_ids'].shape, input_tensors['input_ids'].dtype, input_tensors['input_ids'].device)
|
|
|
if 'pixel_values' in input_tensors:
|
|
|
logger.debug("Pixel values shape: %s, dtype: %s, on device: %s", input_tensors['pixel_values'].shape, input_tensors['pixel_values'].dtype, input_tensors['pixel_values'].device)
|
|
|
|
|
|
|
|
|
except Exception as e:
|
|
|
logger.error("Failed to prepare input tensors (tokenization/image processing): %s", e)
|
|
|
|
|
|
if torch.cuda.is_available(): torch.cuda.empty_cache()
|
|
|
gc.collect()
|
|
|
|
|
|
return {"full_texts": [], "reasoning_steps": [], "final_answers": [], "generated_images": [], "generation_scores": None}
|
|
|
|
|
|
|
|
|
|
|
|
generated_outputs = None
|
|
|
try:
|
|
|
|
|
|
|
|
|
|
|
|
cfg = GenerationConfig()
|
|
|
if self.tokenizer:
|
|
|
|
|
|
cfg.pad_token_id = getattr(self.tokenizer, 'pad_token_id', None)
|
|
|
cfg.eos_token_id = getattr(self.tokenizer, 'eos_token_id', None)
|
|
|
else:
|
|
|
logger.warning("Tokenizer not available, GenerationConfig may lack pad/eos tokens.")
|
|
|
|
|
|
|
|
|
if params:
|
|
|
|
|
|
params_for_gen_config = {k: v for k, v in params.items() if k not in ['self_consistency_enabled', 'requested_chains', 'pad_token_id', 'eos_token_id']}
|
|
|
cfg.update(**params_for_gen_config)
|
|
|
logger.debug("Merged generation_params into GenerationConfig.")
|
|
|
|
|
|
|
|
|
|
|
|
cfg.num_return_sequences = effective_num_return_sequences
|
|
|
if cfg.num_return_sequences > 1 and not cfg.do_sample:
|
|
|
logger.warning("num_return_sequences > 1 but do_sample is False. Generated sequences will be identical.")
|
|
|
if cfg.do_sample and cfg.temperature == 0:
|
|
|
logger.warning("do_sample is True but temperature is 0. Generation will be deterministic.")
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
input_ids_tensor = input_tensors.get('input_ids', torch.tensor([[]]))
|
|
|
input_length = input_ids_tensor.shape[-1] if input_ids_tensor.numel() > 0 else 0
|
|
|
|
|
|
|
|
|
if 'max_new_tokens' in params:
|
|
|
cfg.max_new_tokens = params['max_new_tokens']
|
|
|
|
|
|
|
|
|
|
|
|
if cfg.max_length is None or (input_length + cfg.max_new_tokens) < cfg.max_length:
|
|
|
cfg.max_length = input_length + cfg.max_new_tokens if input_length + cfg.max_new_tokens > 0 else None
|
|
|
logger.debug("Using max_new_tokens from params: %s. Calculated total max_length: %s", cfg.max_new_tokens, cfg.max_length)
|
|
|
|
|
|
elif cfg.max_new_tokens is None:
|
|
|
|
|
|
|
|
|
cfg.max_length = min(self.max_length, cfg.max_length if cfg.max_length is not None else self.max_length)
|
|
|
|
|
|
cfg.max_new_tokens = max(0, cfg.max_length - input_length)
|
|
|
logger.debug("max_new_tokens not set in params or default cfg. Using wrapper max_length: %s. Calculated max_new_tokens: %s", cfg.max_length, cfg.max_new_tokens)
|
|
|
else:
|
|
|
|
|
|
effective_total_length = input_length + cfg.max_new_tokens
|
|
|
if effective_total_length > self.max_length:
|
|
|
logger.warning("Effective total length (%d) exceeds wrapper max_length (%d). Adjusting max_new_tokens.", effective_total_length, self.max_length)
|
|
|
cfg.max_new_tokens = max(0, self.max_length - input_length)
|
|
|
cfg.max_length = input_length + cfg.max_new_tokens if input_length + cfg.max_new_tokens > 0 else None
|
|
|
logger.warning("Adjusted max_new_tokens to %d.", cfg.max_new_tokens)
|
|
|
else:
|
|
|
|
|
|
cfg.max_length = input_length + cfg.max_new_tokens if input_length + cfg.max_new_tokens > 0 else None
|
|
|
logger.debug("Using max_new_tokens from default cfg: %s. Calculated total max_length: %s", cfg.max_new_tokens, cfg.max_length)
|
|
|
|
|
|
|
|
|
|
|
|
if cfg.max_length is None and (input_length + (cfg.max_new_tokens if cfg.max_new_tokens is not None else 0)) > 0:
|
|
|
calculated_max_length = input_length + (cfg.max_new_tokens if cfg.max_new_tokens is not None else 0)
|
|
|
if calculated_max_length > 0:
|
|
|
cfg.max_length = calculated_max_length
|
|
|
else:
|
|
|
cfg.max_length = None
|
|
|
|
|
|
|
|
|
|
|
|
if cfg.max_new_tokens is not None and cfg.max_new_tokens <= 0:
|
|
|
logger.warning("Calculated max_new_tokens is 0 or less. Generation might return only prompt.")
|
|
|
|
|
|
if input_length < self.max_length and self.max_length > 0:
|
|
|
cfg.max_new_tokens = 1
|
|
|
|
|
|
cfg.max_length = input_length + cfg.max_new_tokens
|
|
|
logger.warning("Setting max_new_tokens to 1 to attempt minimal generation.")
|
|
|
else:
|
|
|
|
|
|
cfg.max_new_tokens = 0
|
|
|
logger.warning("Input length is already at max_length or max_length is zero. Cannot generate new tokens (max_new_tokens = 0).")
|
|
|
|
|
|
|
|
|
logger.debug("Final GenerationConfig for this call after resolving params: %s", cfg.to_dict())
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
generated_outputs = self.model.generate(
|
|
|
**input_tensors,
|
|
|
generation_config=cfg,
|
|
|
return_dict_in_generate=True,
|
|
|
output_scores=True
|
|
|
)
|
|
|
logger.info(f"Model generation complete. Generated {len(generated_outputs.sequences)} sequences.")
|
|
|
|
|
|
|
|
|
generation_scores = generated_outputs.scores if hasattr(generated_outputs, 'scores') else None
|
|
|
if generation_scores is not None:
|
|
|
logger.debug("Generation scores available (%d scores tensors).", len(generation_scores))
|
|
|
|
|
|
|
|
|
except Exception as e:
|
|
|
logger.error("Failed during model generation: %s", e)
|
|
|
|
|
|
if torch.cuda.is_available(): torch.cuda.empty_cache()
|
|
|
gc.collect()
|
|
|
|
|
|
return {"full_texts": [], "reasoning_steps": [], "final_answers": [], "generated_images": [], "generation_scores": None}
|
|
|
|
|
|
|
|
|
|
|
|
full_texts: List[str] = []
|
|
|
reasoning_steps: List[List[str]] = []
|
|
|
final_answers: List[Optional[str]] = []
|
|
|
|
|
|
generated_images_list: List[Any] = []
|
|
|
|
|
|
|
|
|
if generated_outputs and hasattr(generated_outputs, 'sequences'):
|
|
|
|
|
|
|
|
|
if self.tokenizer is None:
|
|
|
logger.error("Tokenizer is missing. Cannot decode generated sequences.")
|
|
|
|
|
|
else:
|
|
|
|
|
|
|
|
|
input_ids_tensor = input_tensors.get('input_ids', torch.tensor([[]]))
|
|
|
input_length = input_ids_tensor.shape[-1] if input_ids_tensor.numel() > 0 else 0
|
|
|
logger.debug(f"Input token length determined for prompt removal during decoding: {input_length}")
|
|
|
|
|
|
|
|
|
for i, sequence in enumerate(generated_outputs.sequences):
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
if isinstance(sequence, torch.Tensor):
|
|
|
|
|
|
|
|
|
|
|
|
start_index = max(0, input_length)
|
|
|
|
|
|
decoded_text = self.tokenizer.decode(sequence[start_index:], skip_special_tokens=True)
|
|
|
logger.debug(f"Decoded new tokens for sequence {i} (input length {input_length}, decoded from index {start_index}): {decoded_text[:200]}...")
|
|
|
else:
|
|
|
|
|
|
logger.warning(f"Generated sequence {i} is not a tensor (type: {type(sequence)}). Decoding full sequence and hoping parsing handles it.")
|
|
|
|
|
|
decoded_text = self.tokenizer.decode(sequence, skip_special_tokens=True)
|
|
|
logger.debug(f"Decoded full sequence {i}: {decoded_text[:200]}...")
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
steps, answer, full_output_text_cleaned = self._parse(
|
|
|
decoded_text,
|
|
|
input_text,
|
|
|
full_text_prompt
|
|
|
)
|
|
|
|
|
|
full_texts.append(full_output_text_cleaned)
|
|
|
reasoning_steps.append(steps)
|
|
|
final_answers.append(answer)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
else:
|
|
|
logger.warning("Model generation did not return sequences in expected format or returned no sequences.")
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
if AGI_IMPORTS_SUCCESS and full_texts:
|
|
|
|
|
|
main_output_text = full_texts[0]
|
|
|
|
|
|
if self.memory_engine:
|
|
|
try:
|
|
|
|
|
|
|
|
|
self.memory_engine.observe(main_output_text)
|
|
|
logger.debug("MemoryEngine observed generated output (text).")
|
|
|
except Exception as e:
|
|
|
logger.warning(f"MemoryEngine observe failed: {e}")
|
|
|
|
|
|
try:
|
|
|
|
|
|
if reasoning_steps and reasoning_steps[0]:
|
|
|
|
|
|
valid_steps = [step for step in reasoning_steps[0] if isinstance(step, str) and step.strip()]
|
|
|
if valid_steps:
|
|
|
self.memory_engine.save_reasoning_chain(1, valid_steps)
|
|
|
logger.debug("MemoryEngine saved reasoning chain (from first chain).")
|
|
|
else:
|
|
|
logger.debug("MemoryEngine skipping saving empty or invalid reasoning chain.")
|
|
|
except Exception as e:
|
|
|
logger.warning(f"MemoryEngine save_reasoning_chain failed: {e}")
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
if self.neuro_processor:
|
|
|
try:
|
|
|
|
|
|
generation_experience_detail = f"Generated response (first chain): {main_output_text[:200]}{'...' if len(main_output_text) > 200 else ''}"
|
|
|
|
|
|
self.neuro_processor.record_experience("generation", generation_experience_detail)
|
|
|
logger.debug("NeuroMemoryProcessor recorded generation experience (text).")
|
|
|
except Exception as e:
|
|
|
logger.warning(f"NeuroMemoryProcessor record_experience failed: {e}")
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
if self.agi_enhancer:
|
|
|
try:
|
|
|
|
|
|
enhancer_experience_detail = f"Generated response (first chain): {main_output_text[:200]}{'...' if len(main_output_text) > 200 else ''}"
|
|
|
|
|
|
self.agi_enhancer.log_experience(enhancer_experience_detail)
|
|
|
logger.debug("AGIEnhancer logged experience.")
|
|
|
except Exception as e:
|
|
|
logger.warning(f"AGIEnhancer log_experience failed: {e}")
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
if self.neo_sentient_core:
|
|
|
try:
|
|
|
|
|
|
|
|
|
if hasattr(self.neo_sentient_core, 'process_output'):
|
|
|
self.neo_sentient_core.process_output(main_output_text)
|
|
|
logger.debug("NeoSentientCore processed generated output (text).")
|
|
|
else:
|
|
|
logger.warning("NeoSentientCore does not have a 'process_output' method. Skipping output processing.")
|
|
|
|
|
|
except Exception as e:
|
|
|
logger.warning(f"NeoSentientCore process_output failed: {e}")
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
if torch.cuda.is_available():
|
|
|
try:
|
|
|
torch.cuda.empty_cache()
|
|
|
logger.debug("GPU memory cache cleared after generation attempt.")
|
|
|
except Exception as cleanup_e:
|
|
|
logger.warning(f"Error during cuda empty_cache after generation attempt: {cleanup_e}")
|
|
|
pass
|
|
|
gc.collect()
|
|
|
logger.debug("Garbage collection performed after generation attempt.")
|
|
|
|
|
|
|
|
|
|
|
|
return {
|
|
|
"full_texts": full_texts,
|
|
|
"reasoning_steps": reasoning_steps,
|
|
|
"final_answers": final_answers,
|
|
|
"generation_scores": generation_scores,
|
|
|
|
|
|
"generated_images": generated_images_list
|
|
|
}
|
|
|
|
|
|
|
|
|
def _parse(self, text: str, user_input: str, cot_prompt_text: str) -> Tuple[List[str], Optional[str], str]:
|
|
|
"""
|
|
|
Parses one chainβs generated text into steps + final answer.
|
|
|
Handles artifact cleaning. Attempts to handle potential prompt remnants.
|
|
|
Returns: (steps_list, final_answer_string_or_None, cleaned_body_text)
|
|
|
"""
|
|
|
logger.debug("_parse method called.")
|
|
|
|
|
|
if not isinstance(text, str):
|
|
|
logger.warning(f"Attempted to parse non-string output: {type(text)}. Returning empty.")
|
|
|
return [], None, str(text)
|
|
|
|
|
|
body = text.strip()
|
|
|
|
|
|
|
|
|
for pattern in self._artifact_patterns:
|
|
|
body = pattern.sub("", body)
|
|
|
body = body.strip()
|
|
|
logger.debug(f"Text body after artifact cleanup: {body[:200]}...")
|
|
|
|
|
|
|
|
|
lines = [l.strip() for l in body.splitlines() if l.strip()]
|
|
|
logger.debug(f"Split into {len(lines)} non-empty lines.")
|
|
|
|
|
|
|
|
|
steps: List[str] = []
|
|
|
final_answer: Optional[str] = None
|
|
|
tagged = False
|
|
|
answer_line_index = -1
|
|
|
|
|
|
|
|
|
|
|
|
for i, line in enumerate(lines):
|
|
|
m = self.final_answer_pattern.search(line)
|
|
|
if m:
|
|
|
final_answer = m.group(1).strip()
|
|
|
tagged = True
|
|
|
answer_line_index = i
|
|
|
logger.debug(f"Found final answer tag on line {i}: '{final_answer[:100]}...'")
|
|
|
break
|
|
|
|
|
|
|
|
|
|
|
|
step_lines = []
|
|
|
if tagged and answer_line_index != -1:
|
|
|
|
|
|
step_lines = lines[:answer_line_index]
|
|
|
logger.debug(f"Collecting steps from lines before answer tag (up to line {answer_line_index}).")
|
|
|
else:
|
|
|
|
|
|
step_lines = lines
|
|
|
logger.debug("Final answer tag not found. Collecting steps from all lines matching step pattern.")
|
|
|
|
|
|
|
|
|
|
|
|
for line in step_lines:
|
|
|
m = self._step_pattern.match(line)
|
|
|
if m:
|
|
|
steps.append(m.group(1).strip())
|
|
|
|
|
|
if self.reasoning_steps_limit > 0 and len(steps) >= self.reasoning_steps_limit:
|
|
|
logger.debug("Reached reasoning steps limit (%d). Stopping step collection.", self.reasoning_steps_limit)
|
|
|
break
|
|
|
|
|
|
logger.debug(f"Extracted {len(steps)} reasoning steps.")
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
if not tagged and (final_answer is None or not final_answer.strip()):
|
|
|
logger.debug("Attempting fallback for final answer...")
|
|
|
|
|
|
|
|
|
start_index_for_fallback = answer_line_index if tagged and answer_line_index != -1 else len(lines) -1
|
|
|
for i in range(start_index_for_fallback, -1, -1):
|
|
|
line = lines[i]
|
|
|
|
|
|
if line.strip() and not self._step_pattern.match(line):
|
|
|
|
|
|
fallback_answer_attempt = re.sub(
|
|
|
r"^\s*(?:Answer|Result|Output|Final Answer)\s*[:\-]?\s*",
|
|
|
"",
|
|
|
line,
|
|
|
flags=re.IGNORECASE
|
|
|
).strip()
|
|
|
|
|
|
if fallback_answer_attempt:
|
|
|
final_answer = fallback_answer_attempt
|
|
|
logger.debug("Fallback answer found: '%s'", final_answer[:100])
|
|
|
break
|
|
|
|
|
|
elif line.strip():
|
|
|
final_answer = line.strip()
|
|
|
logger.debug("Using last non-empty, non-step line as fallback answer: '%s'", final_answer[:100])
|
|
|
break
|
|
|
|
|
|
logger.debug(f"Final Answer (after fallback): '{final_answer[:100] if final_answer is not None else 'None'}'")
|
|
|
|
|
|
|
|
|
|
|
|
if final_answer is not None:
|
|
|
final_answer = re.sub(r'[.,;:]+$', '', final_answer).strip()
|
|
|
logger.debug(f"Final Answer (after cleanup): '{final_answer[:100] if final_answer is not None else 'None'}'")
|
|
|
|
|
|
|
|
|
logger.debug("Parsing complete. %d steps, Final Answer: '%s'", len(steps), final_answer[:100] if final_answer is not None else 'None')
|
|
|
|
|
|
return steps, final_answer, body
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|