mirror of
https://github.com/slhaf/Partner.git
synced 2026-05-12 16:53:04 +08:00
feat(chat): support streaming reply in agent turn
This commit is contained in:
@@ -11,6 +11,7 @@ import work.slhaf.partner.api.agent.factory.component.abstracts.AbstractAgentMod
|
||||
import work.slhaf.partner.api.agent.factory.component.abstracts.ActivateModel;
|
||||
import work.slhaf.partner.api.agent.factory.component.annotation.Init;
|
||||
import work.slhaf.partner.api.chat.pojo.Message;
|
||||
import work.slhaf.partner.api.chat.runtime.StreamChatMessageConsumer;
|
||||
import work.slhaf.partner.core.cognition.*;
|
||||
import work.slhaf.partner.runtime.interaction.data.context.PartnerRunningFlowContext;
|
||||
|
||||
@@ -53,11 +54,6 @@ public class CommunicationProducer extends AbstractAgentModule.Running<PartnerRu
|
||||
return "communication_producer";
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean useStreaming() {
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public @NotNull List<Message> modulePrompt() {
|
||||
return List.of(new Message(Message.Character.SYSTEM, MODULE_PROMPT));
|
||||
@@ -70,29 +66,10 @@ public class CommunicationProducer extends AbstractAgentModule.Running<PartnerRu
|
||||
}
|
||||
|
||||
private void executeChat(PartnerRunningFlowContext runningFlowContext) {
|
||||
String responseText = null;
|
||||
|
||||
// TODO considering removing retries in module
|
||||
int count = 0;
|
||||
while (true) {
|
||||
try {
|
||||
// TODO 为各模块提供 emit msg 能力后, 在这里统一接收并分发结构化输出.
|
||||
responseText = this.chat(buildChatMessages(runningFlowContext));
|
||||
log.debug("CommunicationProducer responses: {}", responseText);
|
||||
updateChatMessages(runningFlowContext, responseText);
|
||||
updateContext();
|
||||
break;
|
||||
} catch (Exception e) {
|
||||
count++;
|
||||
log.error("Communicating exception occurred: {}", e.getLocalizedMessage());
|
||||
if (count > 3) {
|
||||
responseText = "CommunicationProducer Failed: " + e.getLocalizedMessage();
|
||||
break;
|
||||
}
|
||||
} finally {
|
||||
updateCoreResponse(runningFlowContext, responseText);
|
||||
}
|
||||
}
|
||||
StreamChatMessageConsumer consumer = ReplyDispatcher.INSTANCE.createConsumer(runningFlowContext.getTarget());
|
||||
this.streamChat(buildChatMessages(runningFlowContext), consumer);
|
||||
updateChatMessages(runningFlowContext, consumer.collectResponse());
|
||||
updateContext();
|
||||
}
|
||||
|
||||
private void updateContext() {
|
||||
|
||||
@@ -0,0 +1,96 @@
|
||||
package work.slhaf.partner.module.communication
|
||||
|
||||
import kotlinx.coroutines.*
|
||||
import kotlinx.coroutines.channels.Channel
|
||||
import work.slhaf.partner.api.agent.runtime.interaction.AgentRuntime
|
||||
import work.slhaf.partner.api.agent.runtime.interaction.data.InteractionEvent.EventStatus
|
||||
import work.slhaf.partner.api.agent.runtime.interaction.data.Reply
|
||||
import work.slhaf.partner.api.chat.runtime.StreamChatMessageConsumer
|
||||
import kotlin.time.Duration.Companion.milliseconds
|
||||
|
||||
object ReplyDispatcher {
|
||||
|
||||
private const val AGGREGATE_WINDOW_MILLIS = 100L
|
||||
|
||||
// TODO 通过配置中心动态指定响应通道
|
||||
private var channelName: String? = null
|
||||
|
||||
private val scope = CoroutineScope(SupervisorJob() + Dispatchers.Default)
|
||||
private val collectorChannel = Channel<ReplyChunk>(Channel.UNLIMITED)
|
||||
|
||||
init {
|
||||
scope.launch {
|
||||
var pendingChunk: ReplyChunk? = null
|
||||
while (true) {
|
||||
val firstChunk = pendingChunk ?: collectorChannel.receiveCatching().getOrNull() ?: break
|
||||
pendingChunk = null
|
||||
val builder = StringBuilder(firstChunk.delta)
|
||||
|
||||
while (true) {
|
||||
val nextChunk = withTimeoutOrNull(AGGREGATE_WINDOW_MILLIS.milliseconds) {
|
||||
collectorChannel.receiveCatching()
|
||||
} ?: break
|
||||
|
||||
if (nextChunk.isClosed) {
|
||||
flush(builder.toString(), firstChunk.target, firstChunk.channelName)
|
||||
return@launch
|
||||
}
|
||||
|
||||
val chunk = nextChunk.getOrNull() ?: break
|
||||
if (chunk.target == firstChunk.target && chunk.channelName == firstChunk.channelName) {
|
||||
builder.append(chunk.delta)
|
||||
} else {
|
||||
pendingChunk = chunk
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
flush(builder.toString(), firstChunk.target, firstChunk.channelName)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun flush(content: String, target: String, channelName: String?) {
|
||||
if (content.isEmpty()) {
|
||||
return
|
||||
}
|
||||
val event = Reply(
|
||||
status = EventStatus.RUNNING,
|
||||
target = target,
|
||||
content = content,
|
||||
mode = Reply.ContentMode.APPEND,
|
||||
done = false
|
||||
)
|
||||
if (channelName.isNullOrBlank()) {
|
||||
AgentRuntime.response(event)
|
||||
} else {
|
||||
AgentRuntime.response(event, channelName)
|
||||
}
|
||||
}
|
||||
|
||||
fun createConsumer(target: String): StreamChatMessageConsumer = ReplyConsumer(
|
||||
collectorChannel = collectorChannel,
|
||||
target = target,
|
||||
channelName = channelName
|
||||
)
|
||||
|
||||
private data class ReplyChunk(
|
||||
val delta: String,
|
||||
val target: String,
|
||||
val channelName: String?
|
||||
)
|
||||
|
||||
private class ReplyConsumer(
|
||||
private val collectorChannel: Channel<ReplyChunk>,
|
||||
private val target: String,
|
||||
private val channelName: String?
|
||||
) : StreamChatMessageConsumer() {
|
||||
|
||||
override fun consumeDelta(delta: String?) {
|
||||
if (delta != null) {
|
||||
collectorChannel.trySend(ReplyChunk(delta, target, channelName)).isSuccess
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user