mirror of
https://github.com/agentscope-ai/ReMe.git
synced 2026-09-07 08:26:06 +00:00
185 lines
6.7 KiB
Python
185 lines
6.7 KiB
Python
"""Common utility functions"""
|
|
|
|
import asyncio
|
|
import hashlib
|
|
from collections.abc import AsyncGenerator, Coroutine
|
|
from typing import Any, Literal
|
|
|
|
import numpy as np
|
|
from loguru import logger
|
|
|
|
from ..enumeration import ChunkEnum
|
|
from ..schema import StreamChunk
|
|
|
|
|
|
def run_coro_safely(coro: Coroutine[Any, Any, Any]) -> Any | asyncio.Task[Any]:
|
|
"""Run a coroutine in the current event loop or a new one if none exists."""
|
|
try:
|
|
# Attempt to retrieve the event loop associated with the current thread
|
|
loop = asyncio.get_running_loop()
|
|
|
|
except RuntimeError:
|
|
# Start a new event loop to run the coroutine to completion
|
|
return asyncio.run(coro)
|
|
|
|
else:
|
|
# Schedule the coroutine as a background task in the active loop
|
|
return loop.create_task(coro)
|
|
|
|
|
|
async def execute_stream_task(
|
|
stream_queue: asyncio.Queue,
|
|
task: asyncio.Task,
|
|
task_name: str | None = None,
|
|
output_format: Literal["str", "bytes", "chunk"] = "str",
|
|
) -> AsyncGenerator[str | bytes | StreamChunk, None]:
|
|
"""
|
|
Core stream flow execution logic.
|
|
|
|
Handles streaming from a queue while monitoring the task completion.
|
|
Properly manages errors and resource cleanup.
|
|
|
|
Args:
|
|
stream_queue: Queue to receive StreamChunk objects from
|
|
task: Background task executing the flow
|
|
task_name: Optional flow name for logging purposes
|
|
output_format: Output format control
|
|
- "str": SSE-formatted string (default)
|
|
- "bytes": SSE-formatted bytes for HTTP responses
|
|
- "chunk": Raw StreamChunk objects
|
|
|
|
Yields:
|
|
- str: SSE-formatted data when output_format="str"
|
|
- bytes: SSE-formatted data when output_format="bytes"
|
|
- StreamChunk: Raw chunk objects when output_format="chunk"
|
|
|
|
Raises:
|
|
Exception: Re-raises any exception from the background task
|
|
"""
|
|
try:
|
|
while True:
|
|
# Wait for next chunk or check if task failed
|
|
get_chunk = asyncio.create_task(stream_queue.get())
|
|
done, _pending = await asyncio.wait({get_chunk, task}, return_when=asyncio.FIRST_COMPLETED)
|
|
|
|
# Priority 1: Check if main task finished (may have exception)
|
|
if task in done:
|
|
# Task finished - check for exceptions first
|
|
exc = task.exception()
|
|
if exc:
|
|
log_msg = f"Task error in {task_name}: {exc}" if task_name else f"Task error: {exc}"
|
|
logger.exception(log_msg)
|
|
raise exc
|
|
|
|
# Task completed successfully - drain remaining chunks if any
|
|
if get_chunk in done:
|
|
chunk: StreamChunk = get_chunk.result()
|
|
if output_format == "chunk":
|
|
yield chunk
|
|
if chunk.done:
|
|
break
|
|
else:
|
|
if chunk.done:
|
|
yield b"data:[DONE]\n\n" if output_format == "bytes" else "data:[DONE]\n\n"
|
|
break
|
|
data = f"data:{chunk.model_dump_json()}\n\n"
|
|
yield data.encode() if output_format == "bytes" else data
|
|
else:
|
|
# No more chunks, task completed
|
|
get_chunk.cancel()
|
|
if output_format == "chunk":
|
|
yield StreamChunk(chunk_type=ChunkEnum.DONE, chunk="", done=True)
|
|
else:
|
|
yield b"data:[DONE]\n\n" if output_format == "bytes" else "data:[DONE]\n\n"
|
|
break
|
|
|
|
elif get_chunk in done:
|
|
# Got a chunk from the queue (task still running)
|
|
chunk: StreamChunk = get_chunk.result()
|
|
|
|
# Handle raw chunk mode
|
|
if output_format == "chunk":
|
|
yield chunk
|
|
if chunk.done:
|
|
break
|
|
continue
|
|
|
|
# Handle SSE format mode (str or bytes)
|
|
if chunk.done:
|
|
yield b"data:[DONE]\n\n" if output_format == "bytes" else "data:[DONE]\n\n"
|
|
break
|
|
|
|
data = f"data:{chunk.model_dump_json()}\n\n"
|
|
yield data.encode() if output_format == "bytes" else data
|
|
|
|
finally:
|
|
# Ensure task is cancelled if still running to avoid resource leaks
|
|
if not task.done():
|
|
task.cancel()
|
|
try:
|
|
await task
|
|
except asyncio.CancelledError:
|
|
pass
|
|
|
|
|
|
def hash_text(text: str) -> str:
|
|
"""Generate SHA-256 hash of text content.
|
|
|
|
Args:
|
|
text: Input text to hash
|
|
|
|
Returns:
|
|
Hexadecimal representation of the SHA-256 hash
|
|
"""
|
|
return hashlib.sha256(text.encode("utf-8")).hexdigest()
|
|
|
|
|
|
def cosine_similarity(vec1: list[float], vec2: list[float]) -> float:
|
|
"""Calculate the cosine similarity between two numeric vectors."""
|
|
if len(vec1) != len(vec2):
|
|
raise ValueError(f"Vectors must have same length: {len(vec1)} != {len(vec2)}")
|
|
|
|
dot_product = sum(a * b for a, b in zip(vec1, vec2))
|
|
magnitude1 = sum(a * a for a in vec1) ** 0.5
|
|
magnitude2 = sum(b * b for b in vec2) ** 0.5
|
|
|
|
if magnitude1 == 0 or magnitude2 == 0:
|
|
return 0.0
|
|
|
|
return dot_product / (magnitude1 * magnitude2)
|
|
|
|
|
|
def batch_cosine_similarity(nd_array1: np.ndarray, nd_array2: np.ndarray) -> np.ndarray:
|
|
"""Calculate cosine similarity matrix between two batches of vectors.
|
|
|
|
Args:
|
|
nd_array1: Matrix of shape (batch_size1, emb_size)
|
|
nd_array2: Matrix of shape (batch_size2, emb_size)
|
|
|
|
Returns:
|
|
Similarity matrix of shape (batch_size1, batch_size2) where
|
|
result[i, j] is the cosine similarity between nd_array1[i] and nd_array2[j]
|
|
|
|
Raises:
|
|
ValueError: If embedding dimensions don't match
|
|
"""
|
|
if nd_array1.shape[1] != nd_array2.shape[1]:
|
|
raise ValueError(f"Embedding dimensions must match: {nd_array1.shape[1]} != {nd_array2.shape[1]}")
|
|
|
|
# Compute dot products: (batch_size1, emb_size) @ (emb_size, batch_size2)
|
|
# Result shape: (batch_size1, batch_size2)
|
|
dot_products = np.dot(nd_array1, nd_array2.T)
|
|
|
|
# Compute L2 norms for each vector
|
|
norms1 = np.linalg.norm(nd_array1, axis=1) # Shape: (batch_size1,)
|
|
norms2 = np.linalg.norm(nd_array2, axis=1) # Shape: (batch_size2,)
|
|
|
|
# Compute outer product of norms: (batch_size1, 1) @ (1, batch_size2)
|
|
# Result shape: (batch_size1, batch_size2)
|
|
norm_products = np.outer(norms1, norms2)
|
|
|
|
# Avoid division by zero
|
|
norm_products = np.where(norm_products == 0, 1e-10, norm_products)
|
|
|
|
# Compute cosine similarities
|
|
return dot_products / norm_products
|