From 0d4a5a0cb5e34b3f4bf2f1b522caebf9ed0de457 Mon Sep 17 00:00:00 2001 From: Xu-create-ops Date: Sun, 20 Jul 2025 13:38:22 +0800 Subject: [PATCH 1/2] Add resource title --- dev-share-api/Controllers/ApiController.cs | 2 + dev-share-api/Data/Entities/Resource.cs | 3 + dev-share-api/Data/Entities/UserInsight.cs | 2 + .../Handle/DatabaseShareChainHandle.cs | 3 +- .../Handle/SummarizeShareChainHandle.cs | 13 +- ...250720042923_AddRescourceTitle.Designer.cs | 105 ++++++++++++++++ .../20250720042923_AddRescourceTitle.cs | 28 +++++ .../20250720053132_AddTime.Designer.cs | 118 ++++++++++++++++++ .../Migrations/20250720053132_AddTime.cs | 63 ++++++++++ .../DevShareDbContextModelSnapshot.cs | 16 +++ dev-share-api/Models/ResourceDto.cs | 1 + dev-share-api/Models/ResourceShareContext.cs | 1 + dev-share-api/Models/SummaryResult.cs | 12 ++ dev-share-api/Services/ResourceService.cs | 9 +- dev-share-api/Services/SummaryService.cs | 97 +++++++++++++- dev-share-api/Services/UserInsightService.cs | 4 +- 16 files changed, 461 insertions(+), 16 deletions(-) create mode 100644 dev-share-api/Migrations/20250720042923_AddRescourceTitle.Designer.cs create mode 100644 dev-share-api/Migrations/20250720042923_AddRescourceTitle.cs create mode 100644 dev-share-api/Migrations/20250720053132_AddTime.Designer.cs create mode 100644 dev-share-api/Migrations/20250720053132_AddTime.cs create mode 100644 dev-share-api/Models/SummaryResult.cs diff --git a/dev-share-api/Controllers/ApiController.cs b/dev-share-api/Controllers/ApiController.cs index 101d5b3..51661d1 100644 --- a/dev-share-api/Controllers/ApiController.cs +++ b/dev-share-api/Controllers/ApiController.cs @@ -85,6 +85,7 @@ await executor.ExecuteAsync(new ResourceShareContext } catch (Exception ex) { + Console.WriteLine(ex.ToString()); task.Status = "failed"; task.Message = ex.Message; } @@ -159,6 +160,7 @@ public async Task Search([FromBody] SearchRequest request) } catch (Exception ex) { + Console.WriteLine(ex.ToString()); return StatusCode(500, "Search failed due to an internal error."); } } diff --git a/dev-share-api/Data/Entities/Resource.cs b/dev-share-api/Data/Entities/Resource.cs index 75497a0..c8b159b 100644 --- a/dev-share-api/Data/Entities/Resource.cs +++ b/dev-share-api/Data/Entities/Resource.cs @@ -13,6 +13,9 @@ public class Resource public long ResourceId { get; set; } public string Url { get; set; } public string NormalizeUrl { get; set; } + public string? Title { get; set; } public string Content { get; set; } public List UserInsights { get; set; } + public DateTime CreatedAt { get; set; } + public DateTime UpdatedAt { get; set; } } \ No newline at end of file diff --git a/dev-share-api/Data/Entities/UserInsight.cs b/dev-share-api/Data/Entities/UserInsight.cs index ab5104a..8946ae2 100644 --- a/dev-share-api/Data/Entities/UserInsight.cs +++ b/dev-share-api/Data/Entities/UserInsight.cs @@ -13,4 +13,6 @@ public class UserInsight public string Content { get; set; } [ForeignKey("ResourceId")] public Resource Resource { get; set; } + public DateTime CreatedAt { get; set; } + public DateTime UpdatedAt { get; set; } } \ No newline at end of file diff --git a/dev-share-api/Handle/DatabaseShareChainHandle.cs b/dev-share-api/Handle/DatabaseShareChainHandle.cs index 49c3fd7..95e31af 100644 --- a/dev-share-api/Handle/DatabaseShareChainHandle.cs +++ b/dev-share-api/Handle/DatabaseShareChainHandle.cs @@ -34,7 +34,8 @@ await _resourceService.AddResourceAsync( { ResourceId = resourceId, Content = context.Summary, - Url = context.Url + Url = context.Url, + Title = context.Title }); await _vectorService.UpsertResourceAsync( resourceId.ToString(), diff --git a/dev-share-api/Handle/SummarizeShareChainHandle.cs b/dev-share-api/Handle/SummarizeShareChainHandle.cs index e2b5ec3..d4aed5c 100644 --- a/dev-share-api/Handle/SummarizeShareChainHandle.cs +++ b/dev-share-api/Handle/SummarizeShareChainHandle.cs @@ -26,14 +26,11 @@ public override async Task IsSkip(ResourceShareContext context) protected override async Task ProcessAsync(ResourceShareContext context) { - var prompt = new StringBuilder() - .AppendLine("You will receive an input text and your task is to summarize the article in no more than 100 words.") - .AppendLine("Only return the summary. Do not include any explanation.") - .AppendLine("# Article content:") - .AppendLine($"{context.ExtractResult}") - .ToString(); - var summary = await _summaryService.SummarizeAsync(prompt); - context.Summary = summary; + + var summary = await _summaryService.SummarizeAsync(context.ExtractResult); + context.Summary = summary.Summary; + context.Title = summary.Title; return HandlerResult.Success(); } + } \ No newline at end of file diff --git a/dev-share-api/Migrations/20250720042923_AddRescourceTitle.Designer.cs b/dev-share-api/Migrations/20250720042923_AddRescourceTitle.Designer.cs new file mode 100644 index 0000000..4376361 --- /dev/null +++ b/dev-share-api/Migrations/20250720042923_AddRescourceTitle.Designer.cs @@ -0,0 +1,105 @@ +// +using Data; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Metadata; +using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; + +#nullable disable + +namespace dev_share_api.Migrations +{ + [DbContext(typeof(DevShareDbContext))] + [Migration("20250720042923_AddRescourceTitle")] + partial class AddRescourceTitle + { + /// + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasAnnotation("ProductVersion", "9.0.3") + .HasAnnotation("Relational:MaxIdentifierLength", 128); + + SqlServerModelBuilderExtensions.UseIdentityColumns(modelBuilder); + + modelBuilder.Entity("Entities.Resource", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("Content") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("NormalizeUrl") + .IsRequired() + .HasColumnType("nvarchar(450)"); + + b.Property("ResourceId") + .HasColumnType("bigint"); + + b.Property("Title") + .HasColumnType("nvarchar(max)"); + + b.Property("Url") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.HasKey("Id"); + + b.HasIndex("NormalizeUrl") + .IsUnique(); + + b.HasIndex("ResourceId") + .IsUnique(); + + b.ToTable("Resources"); + }); + + modelBuilder.Entity("Entities.UserInsight", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("Content") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("ResourceId") + .HasColumnType("bigint"); + + b.HasKey("Id"); + + b.HasIndex("ResourceId"); + + b.ToTable("UserInsights"); + }); + + modelBuilder.Entity("Entities.UserInsight", b => + { + b.HasOne("Entities.Resource", "Resource") + .WithMany("UserInsights") + .HasForeignKey("ResourceId") + .HasPrincipalKey("ResourceId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Resource"); + }); + + modelBuilder.Entity("Entities.Resource", b => + { + b.Navigation("UserInsights"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/dev-share-api/Migrations/20250720042923_AddRescourceTitle.cs b/dev-share-api/Migrations/20250720042923_AddRescourceTitle.cs new file mode 100644 index 0000000..46aa9f4 --- /dev/null +++ b/dev-share-api/Migrations/20250720042923_AddRescourceTitle.cs @@ -0,0 +1,28 @@ +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +namespace dev_share_api.Migrations +{ + /// + public partial class AddRescourceTitle : Migration + { + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.AddColumn( + name: "Title", + table: "Resources", + type: "nvarchar(max)", + nullable: true); + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropColumn( + name: "Title", + table: "Resources"); + } + } +} diff --git a/dev-share-api/Migrations/20250720053132_AddTime.Designer.cs b/dev-share-api/Migrations/20250720053132_AddTime.Designer.cs new file mode 100644 index 0000000..4256192 --- /dev/null +++ b/dev-share-api/Migrations/20250720053132_AddTime.Designer.cs @@ -0,0 +1,118 @@ +// +using System; +using Data; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Metadata; +using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; + +#nullable disable + +namespace dev_share_api.Migrations +{ + [DbContext(typeof(DevShareDbContext))] + [Migration("20250720053132_AddTime")] + partial class AddTime + { + /// + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasAnnotation("ProductVersion", "9.0.3") + .HasAnnotation("Relational:MaxIdentifierLength", 128); + + SqlServerModelBuilderExtensions.UseIdentityColumns(modelBuilder); + + modelBuilder.Entity("Entities.Resource", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("Content") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("CreatedAt") + .HasColumnType("datetime2"); + + b.Property("NormalizeUrl") + .IsRequired() + .HasColumnType("nvarchar(450)"); + + b.Property("ResourceId") + .HasColumnType("bigint"); + + b.Property("Title") + .HasColumnType("nvarchar(max)"); + + b.Property("UpdatedAt") + .HasColumnType("datetime2"); + + b.Property("Url") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.HasKey("Id"); + + b.HasIndex("NormalizeUrl") + .IsUnique(); + + b.HasIndex("ResourceId") + .IsUnique(); + + b.ToTable("Resources"); + }); + + modelBuilder.Entity("Entities.UserInsight", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("Content") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("CreatedAt") + .HasColumnType("datetime2"); + + b.Property("ResourceId") + .HasColumnType("bigint"); + + b.Property("UpdatedAt") + .HasColumnType("datetime2"); + + b.HasKey("Id"); + + b.HasIndex("ResourceId"); + + b.ToTable("UserInsights"); + }); + + modelBuilder.Entity("Entities.UserInsight", b => + { + b.HasOne("Entities.Resource", "Resource") + .WithMany("UserInsights") + .HasForeignKey("ResourceId") + .HasPrincipalKey("ResourceId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Resource"); + }); + + modelBuilder.Entity("Entities.Resource", b => + { + b.Navigation("UserInsights"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/dev-share-api/Migrations/20250720053132_AddTime.cs b/dev-share-api/Migrations/20250720053132_AddTime.cs new file mode 100644 index 0000000..ad2da8e --- /dev/null +++ b/dev-share-api/Migrations/20250720053132_AddTime.cs @@ -0,0 +1,63 @@ +using System; +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +namespace dev_share_api.Migrations +{ + /// + public partial class AddTime : Migration + { + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.AddColumn( + name: "CreatedAt", + table: "UserInsights", + type: "datetime2", + nullable: false, + defaultValue: new DateTime(1, 1, 1, 0, 0, 0, 0, DateTimeKind.Unspecified)); + + migrationBuilder.AddColumn( + name: "UpdatedAt", + table: "UserInsights", + type: "datetime2", + nullable: false, + defaultValue: new DateTime(1, 1, 1, 0, 0, 0, 0, DateTimeKind.Unspecified)); + + migrationBuilder.AddColumn( + name: "CreatedAt", + table: "Resources", + type: "datetime2", + nullable: false, + defaultValue: new DateTime(1, 1, 1, 0, 0, 0, 0, DateTimeKind.Unspecified)); + + migrationBuilder.AddColumn( + name: "UpdatedAt", + table: "Resources", + type: "datetime2", + nullable: false, + defaultValue: new DateTime(1, 1, 1, 0, 0, 0, 0, DateTimeKind.Unspecified)); + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropColumn( + name: "CreatedAt", + table: "UserInsights"); + + migrationBuilder.DropColumn( + name: "UpdatedAt", + table: "UserInsights"); + + migrationBuilder.DropColumn( + name: "CreatedAt", + table: "Resources"); + + migrationBuilder.DropColumn( + name: "UpdatedAt", + table: "Resources"); + } + } +} diff --git a/dev-share-api/Migrations/DevShareDbContextModelSnapshot.cs b/dev-share-api/Migrations/DevShareDbContextModelSnapshot.cs index 83e375f..6c1f294 100644 --- a/dev-share-api/Migrations/DevShareDbContextModelSnapshot.cs +++ b/dev-share-api/Migrations/DevShareDbContextModelSnapshot.cs @@ -1,4 +1,5 @@ // +using System; using Data; using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore.Infrastructure; @@ -33,6 +34,9 @@ protected override void BuildModel(ModelBuilder modelBuilder) .IsRequired() .HasColumnType("nvarchar(max)"); + b.Property("CreatedAt") + .HasColumnType("datetime2"); + b.Property("NormalizeUrl") .IsRequired() .HasColumnType("nvarchar(450)"); @@ -40,6 +44,12 @@ protected override void BuildModel(ModelBuilder modelBuilder) b.Property("ResourceId") .HasColumnType("bigint"); + b.Property("Title") + .HasColumnType("nvarchar(max)"); + + b.Property("UpdatedAt") + .HasColumnType("datetime2"); + b.Property("Url") .IsRequired() .HasColumnType("nvarchar(max)"); @@ -67,9 +77,15 @@ protected override void BuildModel(ModelBuilder modelBuilder) .IsRequired() .HasColumnType("nvarchar(max)"); + b.Property("CreatedAt") + .HasColumnType("datetime2"); + b.Property("ResourceId") .HasColumnType("bigint"); + b.Property("UpdatedAt") + .HasColumnType("datetime2"); + b.HasKey("Id"); b.HasIndex("ResourceId"); diff --git a/dev-share-api/Models/ResourceDto.cs b/dev-share-api/Models/ResourceDto.cs index 8fecada..4e8ed90 100644 --- a/dev-share-api/Models/ResourceDto.cs +++ b/dev-share-api/Models/ResourceDto.cs @@ -5,6 +5,7 @@ public class ResourceDto public long ResourceId { get; set; } public required string Url { get; set; } public string? NormalizeUrl { get; set; } + public string? Title { get; set; } public required string Content { get; set; } public List? UserInsights { get; set; } } \ No newline at end of file diff --git a/dev-share-api/Models/ResourceShareContext.cs b/dev-share-api/Models/ResourceShareContext.cs index fc8c1de..37f1774 100644 --- a/dev-share-api/Models/ResourceShareContext.cs +++ b/dev-share-api/Models/ResourceShareContext.cs @@ -7,6 +7,7 @@ public class ResourceShareContext public string? Url { get; set; } public string? Insight { get; set; } public string? Summary { get; set; } + public string? Title { get; set; } public Dictionary? ResourceVectors { get; set; } public Dictionary? InsightVectors { get; set; } public ResourceDto? ExistingResource { get; set; } diff --git a/dev-share-api/Models/SummaryResult.cs b/dev-share-api/Models/SummaryResult.cs new file mode 100644 index 0000000..5db8594 --- /dev/null +++ b/dev-share-api/Models/SummaryResult.cs @@ -0,0 +1,12 @@ +using System.Text.Json.Serialization; + +namespace Models; + +public class SummaryResult +{ + [JsonPropertyName("summary")] + public string Summary { get; set; } + + [JsonPropertyName("title")] + public string Title { get; set; } +} \ No newline at end of file diff --git a/dev-share-api/Services/ResourceService.cs b/dev-share-api/Services/ResourceService.cs index 2f1fb0c..009118f 100644 --- a/dev-share-api/Services/ResourceService.cs +++ b/dev-share-api/Services/ResourceService.cs @@ -21,7 +21,10 @@ public async Task AddResourceAsync(ResourceDto resourceDto) ResourceId = resourceDto.ResourceId, NormalizeUrl = resourceDto.NormalizeUrl, Url = resourceDto.Url, - Content = resourceDto.Content + Content = resourceDto.Content, + Title = resourceDto.Title, + CreatedAt = DateTime.UtcNow, + UpdatedAt = DateTime.UtcNow, }); await _dbContext.SaveChangesAsync(); } @@ -35,7 +38,8 @@ public async Task AddResourceAsync(ResourceDto resourceDto) ResourceId = resource.ResourceId, Url = resource.Url, NormalizeUrl = resource.NormalizeUrl, - Content = resource.Content + Content = resource.Content, + Title = resource.Title }).FirstOrDefaultAsync(); } @@ -49,6 +53,7 @@ public async Task AddResourceAsync(ResourceDto resourceDto) Url = resource.Url, NormalizeUrl = resource.NormalizeUrl, Content = resource.Content, + Title = resource.Title, UserInsights = resource.UserInsights .Select(insight => new UserInsightDto { diff --git a/dev-share-api/Services/SummaryService.cs b/dev-share-api/Services/SummaryService.cs index a961a6b..473dd42 100644 --- a/dev-share-api/Services/SummaryService.cs +++ b/dev-share-api/Services/SummaryService.cs @@ -1,11 +1,13 @@ +using System.Text.Json; using Azure.AI.OpenAI; +using Models; using OpenAI.Chat; namespace Services; public interface ISummaryService { - Task SummarizeAsync(string article); + Task SummarizeAsync(string content); } public class SummaryService : ISummaryService @@ -18,9 +20,96 @@ public SummaryService(AzureOpenAIClient openAIClient) _client = openAIClient; } - public async Task SummarizeAsync(string prompt) + public async Task SummarizeAsync(string content) { - ChatCompletion response = await _client.GetChatClient(deploymentName: _deploymentName).CompleteChatAsync(prompt); - return response.Content[0].Text; + + var messages = new List + { + new SystemChatMessage($@" + You are a helpful summarization assistant. + Your task is to: + 1. Summarize the article in no more than 100 words. + 2. Extract or infer a clear, appropriate title, no more than 12 words. + + Always call the `generate_summary` function with your result in JSON: + {{ + ""summary"": string, + ""title"": string + }} + + Guidelines: + - Avoid fabricating content. Be brief and accurate. Do not include any explanation. + - If the article lacks detail, summarize what’s available. + - Never return plain text. Always return structured JSON using the `generate_summary` function. + "), + new UserChatMessage(content) + }; + + var tool = CreateGenerateSummaryTool(); + + return await CallToolAndDeserializeAsync( + toolFunctionName: "generate_summary", + messages: messages, + tool: tool + ); + + } + + + private ChatTool CreateGenerateSummaryTool() + { + return ChatTool.CreateFunctionTool( + functionName: "generate_summary", + functionDescription: "Generates a short summary and a suitable title for the provided article.", + functionParameters: BinaryData.FromObjectAsJson(new + { + type = "object", + properties = new + { + summary = new + { + type = "string", + description = "A concise summary of the article, no more than 100 words." + }, + title = new + { + type = "string", + description = "A short, clear, and appropriate title that reflects the article's content, no more than 12 words." + } + }, + required = new[] { "summary", "title" } + }) + ); + } + + public async Task CallToolAndDeserializeAsync( + string toolFunctionName, + List messages, + ChatTool tool) + { + var client = _client.GetChatClient(deploymentName: _deploymentName); + ChatCompletionOptions options = new() + { + Tools = { tool } + }; + ChatCompletion response = await client.CompleteChatAsync( + messages: messages, + options + ); + + var toolCall = response.ToolCalls.FirstOrDefault(tc => tc.FunctionName == toolFunctionName); + if (toolCall == null) + { + throw new InvalidOperationException("No function call response found."); + } + + var json = toolCall.FunctionArguments.ToString(); + var result = JsonSerializer.Deserialize(json); + if (result == null) + { + throw new InvalidOperationException("Deserialization failed."); + } + + return result; } } diff --git a/dev-share-api/Services/UserInsightService.cs b/dev-share-api/Services/UserInsightService.cs index 6de068f..eeb930a 100644 --- a/dev-share-api/Services/UserInsightService.cs +++ b/dev-share-api/Services/UserInsightService.cs @@ -19,7 +19,9 @@ public async Task AddUserInsightAsync(UserInsightDto userInsight) _dbContext.UserInsights.Add(new UserInsight { ResourceId = userInsight.ResourceId, - Content = userInsight.Content + Content = userInsight.Content, + CreatedAt = DateTime.UtcNow, + UpdatedAt = DateTime.UtcNow, }); await _dbContext.SaveChangesAsync(); } From 03346194903c4d95cedf0c25372e430be5eaaa89 Mon Sep 17 00:00:00 2001 From: Xu-create-ops Date: Sun, 20 Jul 2025 13:39:15 +0800 Subject: [PATCH 2/2] Add front title. --- dev-share-ui/lib/types.ts | 1 + dev-share-ui/services/search-service.tsx | 2 +- dev-share-ui/services/share-service.tsx | 2 +- 3 files changed, 3 insertions(+), 2 deletions(-) diff --git a/dev-share-ui/lib/types.ts b/dev-share-ui/lib/types.ts index fb9d33d..1e29f31 100644 --- a/dev-share-ui/lib/types.ts +++ b/dev-share-ui/lib/types.ts @@ -21,4 +21,5 @@ export interface Resource { export interface VectorSearchResultDTO { url: string; content: string; + title: string; } \ No newline at end of file diff --git a/dev-share-ui/services/search-service.tsx b/dev-share-ui/services/search-service.tsx index 28cd704..6b8b65e 100644 --- a/dev-share-ui/services/search-service.tsx +++ b/dev-share-ui/services/search-service.tsx @@ -19,7 +19,7 @@ export async function searchResources(query: string): Promise { return dtos.map(dto => ({ id: crypto.randomUUID(), - title: dto.content.slice(0, 80), + title: dto.title, description: dto.content, url: dto.url, imageUrl: "", diff --git a/dev-share-ui/services/share-service.tsx b/dev-share-ui/services/share-service.tsx index 93e6dfc..f018569 100644 --- a/dev-share-ui/services/share-service.tsx +++ b/dev-share-ui/services/share-service.tsx @@ -8,7 +8,7 @@ export async function submitSharedResource(url: string, comment: string): Promis headers: { "Content-Type": "application/json", }, - body: JSON.stringify({ url, comment }), + body: JSON.stringify({ url: url, insight: comment }), signal: controller.signal, });