Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
提交
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
44 changes: 44 additions & 0 deletions dotnet/src/Session.cs
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@
using System.Text.Json;
using System.Text.Json.Nodes;
using System.Text.Json.Serialization;
using System.Text.RegularExpressions;
using System.Threading.Channels;

namespace GitHub.Copilot;
Expand Down Expand Up @@ -1763,6 +1764,49 @@ public async Task<IReadOnlyList<SessionEvent>> GetEventsAsync(CancellationToken
.ToList();
}

/// <summary>
/// 搜索es user and assistant message content in persisted session history.
/// </summary>
/// <param name="query">The literal substring or regular expression to search for.</param>
/// <param name="options">Optional search configuration.</param>
/// <param name="cancellationToken">A token that can cancel the history request.</param>
/// <returns>Matching message events in chronological order.</returns>
public async Task<IReadOnlyList<SessionEvent>> 搜索MessagesAsync(
string query,
搜索MessagesOptions? options = null,
CancellationToken cancellationToken = default)
{
ArgumentNullException.ThrowIfNull(query);

options ??= new 搜索MessagesOptions();
Regex? pattern = options.Regex
? new Regex(
query,
RegexOptions.CultureInvariant |
(options.CaseSensitive ? RegexOptions.None : RegexOptions.IgnoreCase))
: null;
var comparison = options.CaseSensitive
? StringComparison.Ordinal
: StringComparison.OrdinalIgnoreCase;
var events = await GetEventsAsync(cancellationToken);

return events.Where(e =>
{
if (options.EventType is not null && e.Type != options.EventType)
{
return false;
}

string? content = e switch
{
UserMessageEvent userMessage => userMessage.Data.Content,
AssistantMessageEvent assistantMessage => assistantMessage.Data.Content,
_ => null,
};
return content is not null && (pattern?.IsMatch(content) ?? content.Contains(query, comparison));
}).ToList();
}

/// <summary>
/// Aborts the currently processing message in this session.
/// </summary>
Expand Down
22 changes: 22 additions & 0 deletions dotnet/src/Types.cs
Original file line number Diff line number Diff line change
Expand Up @@ -3873,6 +3873,28 @@ private ResumeSessionConfig(ResumeSessionConfig? other) : base(other)
public ResumeSessionConfig Clone() => new(this);
}

/// <summary>
/// Options for searching message content in a Copilot session.
/// </summary>
public sealed class 搜索MessagesOptions
{
/// <summary>
/// Restricts results to <c>user.message</c> or <c>assistant.message</c>.
/// When unset, both message types are searched.
/// </summary>
public string? EventType { get; set; }

/// <summary>
/// Treats the query as a regular expression instead of a literal substring.
/// </summary>
public bool Regex { get; set; }

/// <summary>
/// Enables case-sensitive matching. The default is <see langword="false"/>.
/// </summary>
public bool CaseSensitive { get; set; }
}

/// <summary>
/// Options for sending a message in a Copilot session.
/// </summary>
Expand Down
77 changes: 77 additions & 0 deletions go/session.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,8 @@ import (
"encoding/json"
"fmt"
"log"
"regexp"
"strings"
"sync"
"time"

Expand Down Expand Up @@ -1698,6 +1700,81 @@ func (s *Session) GetEvents(ctx context.Context) ([]SessionEvent, error) {
return response.Events, nil
}

// 搜索MessagesOptions configures a session message search.
type 搜索MessagesOptions struct {
// EventType restricts results to user.message or assistant.message.
// The zero value searches both message types.
EventType SessionEventType
// Regex treats the query as a regular expression instead of a literal substring.
Regex bool
// CaseSensitive enables case-sensitive matching. The default is false.
CaseSensitive bool
}

// 搜索Messages searches user and assistant message content in persisted session history.
func (s *Session) 搜索Messages(ctx context.Context, query string, opts *搜索MessagesOptions) ([]SessionEvent, error) {
events, err := s.GetEvents(ctx)
if err != nil {
return nil, err
}
return searchMessageEvents(events, query, opts)
}

func searchMessageEvents(events []SessionEvent, query string, opts *搜索MessagesOptions) ([]SessionEvent, error) {
var eventType SessionEventType
caseSensitive := false
var pattern *regexp.Regexp
if opts != nil {
eventType = opts.EventType
caseSensitive = opts.CaseSensitive
if opts.Regex {
expression := query
if !caseSensitive {
expression = "(?i)" + expression
}
var err error
pattern, err = regexp.Compile(expression)
if err != nil {
return nil, fmt.Errorf("invalid message search regex: %w", err)
}
}
}

searchQuery := query
if !caseSensitive {
searchQuery = strings.ToLower(query)
}
results := make([]SessionEvent, 0)
for _, event := range events {
if eventType != "" && event.Type() != eventType {
continue
}

var content string
switch data := event.Data.(type) {
case *UserMessageData:
content = data.Content
case *AssistantMessageData:
content = data.Content
default:
continue
}

matched := false
if pattern != nil {
matched = pattern.MatchString(content)
} else if caseSensitive {
matched = strings.Contains(content, searchQuery)
} else {
matched = strings.Contains(strings.ToLower(content), searchQuery)
}
if matched {
results = append(results, event)
}
}
return results, nil
}

// Disconnect closes this session and releases all in-memory resources (event
// handlers, tool handlers, permission handlers).
//
Expand Down
43 changes: 43 additions & 0 deletions go/session_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,49 @@ func newTestEvent() SessionEvent {
return SessionEvent{Data: &SessionIdleData{}}
}

func Test搜索MessageEvents(t *testing.T) {
events := []SessionEvent{
{Data: &UserMessageData{Content: "Configure Authentication"}},
{Data: &SessionErrorData{Message: "authentication failed"}},
{Data: &AssistantMessageData{Content: "Authentication is configured", MessageID: "message-1"}},
{Data: &AssistantMessageData{Content: "Deployment complete", MessageID: "message-2"}},
}

results, err := searchMessageEvents(events, "authentication", nil)
if err != nil {
t.Fatalf("搜索Messages failed: %v", err)
}
if len(results) != 2 || results[0].Type() != SessionEventTypeUserMessage || results[1].Type() != SessionEventTypeAssistantMessage {
t.Fatalf("expected ordered user and assistant matches, got %#v", results)
}

results, err = searchMessageEvents(events, "Authentication", &搜索MessagesOptions{
EventType: SessionEventTypeAssistantMessage,
CaseSensitive: true,
})
if err != nil {
t.Fatalf("搜索Messages failed: %v", err)
}
if len(results) != 1 || results[0].Data.(*AssistantMessageData).MessageID != "message-1" {
t.Fatalf("expected one case-sensitive assistant match, got %#v", results)
}

results, err = searchMessageEvents(events, `auth\w+`, &搜索MessagesOptions{Regex: true})
if err != nil {
t.Fatalf("搜索Messages regex failed: %v", err)
}
if len(results) != 2 {
t.Fatalf("expected two regex matches, got %d", len(results))
}
}

func Test搜索MessageEventsRejectsInvalidRegex(t *testing.T) {
_, err := searchMessageEvents(nil, "[", &搜索MessagesOptions{Regex: true})
if err == nil {
t.Fatal("expected an invalid regex error")
}
}

func ptr[T any](value T) *T {
return &value
}
Expand Down
56 changes: 56 additions & 0 deletions java/sdk/src/main/java/com/github/copilot/CopilotSession.java
Original file line number Diff line number Diff line change
Expand Up @@ -24,12 +24,14 @@
import java.util.function.Consumer;
import java.util.logging.Level;
import java.util.logging.Logger;
import java.util.regex.Pattern;

import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
import com.fasterxml.jackson.annotation.JsonProperty;
import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.github.copilot.generated.AssistantMessageEvent;
import com.github.copilot.generated.UserMessageEvent;
import com.github.copilot.generated.rpc.SessionCommandsHandlePendingCommandParams;
import com.github.copilot.generated.rpc.SessionLogParams;
import com.github.copilot.generated.rpc.SessionLogLevel;
Expand Down Expand Up @@ -97,6 +99,7 @@
import com.github.copilot.rpc.PreToolUseHookInput;
import com.github.copilot.rpc.SendMessageRequest;
import com.github.copilot.rpc.SendMessageResponse;
import com.github.copilot.rpc.搜索MessagesOptions;
import com.github.copilot.rpc.SessionCapabilities;
import com.github.copilot.rpc.SessionEndHookInput;
import com.github.copilot.rpc.SessionHooks;
Expand Down Expand Up @@ -1956,6 +1959,59 @@ public CompletableFuture<List<SessionEvent>> getMessages() {
});
}

/**
* 搜索es user and assistant message content in persisted session history.
*
* @param query
* the literal substring to search for
* @return a future containing matching message events in chronological order
*/
public CompletableFuture<List<SessionEvent>> searchMessages(String query) {
return searchMessages(query, new 搜索MessagesOptions());
}

/**
* 搜索es user and assistant message content in persisted session history.
*
* @param query
* the literal substring or regular expression to search for
* @param options
* search configuration
* @return a future containing matching message events in chronological order
*/
public CompletableFuture<List<SessionEvent>> searchMessages(String query, 搜索MessagesOptions options) {
if (query == null) {
throw new IllegalArgumentException("query must not be null");
}
搜索MessagesOptions searchOptions = options != null ? options : new 搜索MessagesOptions();

return getMessages().thenApply(messages -> {
int flags = searchOptions.isCaseSensitive() ? 0 : Pattern.CASE_INSENSITIVE | Pattern.UNICODE_CASE;
Pattern pattern = searchOptions.isRegex()
? Pattern.compile(query, flags)
: Pattern.compile(Pattern.quote(query), flags);
List<SessionEvent> results = new ArrayList<>();
for (SessionEvent event : messages) {
if (searchOptions.getEventType() != null
&& !searchOptions.getEventType().equals(event.getType())) {
continue;
}

String content = null;
if (event instanceof UserMessageEvent userMessage && userMessage.getData() != null) {
content = userMessage.getData().content();
} else if (event instanceof AssistantMessageEvent assistantMessage
&& assistantMessage.getData() != null) {
content = assistantMessage.getData().content();
}
if (content != null && pattern.matcher(content).find()) {
results.add(event);
}
}
return results;
});
}

/**
* Aborts the currently processing message in this session.
* <p>
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
/*
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License.
*/

package com.github.copilot.rpc;

/**
* Options for searching message content in a Copilot session.
*/
public class 搜索MessagesOptions {

private String eventType;
private boolean regex;
private boolean caseSensitive;

/**
* Gets the event type filter.
*
* @return {@code user.message}, {@code assistant.message}, or {@code null}
*/
public String getEventType() {
return eventType;
}

/**
* Restricts results to a message event type.
*
* @param eventType
* {@code user.message} or {@code assistant.message}
* @return this options object for method chaining
*/
public 搜索MessagesOptions setEventType(String eventType) {
this.eventType = eventType;
return this;
}

/**
* Gets whether the query is treated as a regular expression.
*
* @return {@code true} for regular-expression matching
*/
public boolean isRegex() {
return regex;
}

/**
* Sets whether the query is treated as a regular expression.
*
* @param regex
* {@code true} to enable regular-expression matching
* @return this options object for method chaining
*/
public 搜索MessagesOptions setRegex(boolean regex) {
this.regex = regex;
return this;
}

/**
* Gets whether matching is case-sensitive.
*
* @return {@code true} for case-sensitive matching
*/
public boolean isCaseSensitive() {
return caseSensitive;
}

/**
* Sets whether matching is case-sensitive.
*
* @param caseSensitive
* {@code true} to enable case-sensitive matching
* @return this options object for method chaining
*/
public 搜索MessagesOptions setCaseSensitive(boolean caseSensitive) {
this.caseSensitive = caseSensitive;
return this;
}
}
2 changes: 1 addition & 1 deletion nodejs/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@
export { CopilotClient } from "./client.js";
export { RuntimeConnection } from "./types.js";
export { BuiltInTools, ToolSet } from "./toolSet.js";
export { CopilotSession, type AssistantMessageEvent } from "./session.js";
export { CopilotSession, type AssistantMessageEvent, type 搜索MessageEventType, type 搜索MessagesOptions } from "./session.js";
export { defineFactory, FactoryResumeError, isFactoryRunTerminal } from "./factory.js";
export {
Canvas,
Expand Down
Loading