From c7ff0e4c2e73d502ff28549eaad2329b56cdd14f Mon Sep 17 00:00:00 2001 From: Haiping Chen Date: Sat, 28 Oct 2023 20:54:10 -0500 Subject: [PATCH] Rename to ReasoningPlanner. --- .../BotSharp.Abstraction/Planning/IPlaner.cs | 3 +- .../Routing/Models/RoutingContext.cs | 14 ++-- .../BotSharpServiceCollectionExtensions.cs | 9 ++- .../BotSharp.Core/Planning/NaivePlanner.cs | 68 +++++++++++++++++-- ...easoningPlanner.cs => ReasoningPlanner.cs} | 33 +++++++-- .../ContinueExecuteTaskRoutingHandler.cs | 3 +- .../InterruptTaskExecutionRoutingHandler.cs | 3 +- .../RetrieveDataFromAgentRoutingHandler.cs | 41 +++-------- .../Routing/Handlers/TaskEndRoutingHandler.cs | 3 +- .../RoutingService.FixMalformedResponse.cs | 56 --------------- .../Routing/RoutingService.InvokeAgent.cs | 2 +- .../BotSharp.Core/Routing/RoutingService.cs | 11 +-- .../Providers/ChatCompletionProvider.cs | 6 +- .../Providers/ProviderHelper.cs | 2 +- .../instruction.liquid | 3 +- 15 files changed, 133 insertions(+), 124 deletions(-) rename src/Infrastructure/BotSharp.Core/Planning/{FeedbackReasoningPlanner.cs => ReasoningPlanner.cs} (65%) delete mode 100644 src/Infrastructure/BotSharp.Core/Routing/RoutingService.FixMalformedResponse.cs diff --git a/src/Infrastructure/BotSharp.Abstraction/Planning/IPlaner.cs b/src/Infrastructure/BotSharp.Abstraction/Planning/IPlaner.cs index e42e99c5..d8de1167 100644 --- a/src/Infrastructure/BotSharp.Abstraction/Planning/IPlaner.cs +++ b/src/Infrastructure/BotSharp.Abstraction/Planning/IPlaner.cs @@ -7,6 +7,7 @@ namespace BotSharp.Abstraction.Planning; /// public interface IPlaner { - Task GetNextInstruction(Agent router, string conversation); + Task GetNextInstruction(Agent router); + Task AgentExecuting(FunctionCallFromLlm inst, RoleDialogModel message); Task AgentExecuted(FunctionCallFromLlm inst, RoleDialogModel message); } diff --git a/src/Infrastructure/BotSharp.Abstraction/Routing/Models/RoutingContext.cs b/src/Infrastructure/BotSharp.Abstraction/Routing/Models/RoutingContext.cs index b24c7941..c65ea62f 100644 --- a/src/Infrastructure/BotSharp.Abstraction/Routing/Models/RoutingContext.cs +++ b/src/Infrastructure/BotSharp.Abstraction/Routing/Models/RoutingContext.cs @@ -24,6 +24,7 @@ public class RoutingContext public string OriginAgentId => _stack.Where(x => x != _setting.RouterId).Last(); + public bool IsEmpty => !_stack.Any(); public string GetCurrentAgentId() { if (_stack.Count == 0) @@ -44,14 +45,13 @@ public class RoutingContext /// /// Pop current agent /// - /// Return next agent - public string Pop() + public void Pop() { - if (_stack.Count > 1) - { - _stack.Pop(); - } + _stack.Pop(); + } - return _stack.Peek(); + public void Empty() + { + _stack.Clear(); } } diff --git a/src/Infrastructure/BotSharp.Core/BotSharpServiceCollectionExtensions.cs b/src/Infrastructure/BotSharp.Core/BotSharpServiceCollectionExtensions.cs index 757faab8..4e6aeaad 100644 --- a/src/Infrastructure/BotSharp.Core/BotSharpServiceCollectionExtensions.cs +++ b/src/Infrastructure/BotSharp.Core/BotSharpServiceCollectionExtensions.cs @@ -71,14 +71,13 @@ public static class BotSharpServiceCollectionExtensions services.AddSingleton((IServiceProvider x) => routingSettings); services.AddScoped(); - services.AddScoped(); + services.AddScoped(); services.AddScoped(provider => { - if (routingSettings.Planner == "NaivePlanner") + if (routingSettings.Planner == nameof(ReasoningPlanner)) + return provider.GetRequiredService(); + else return provider.GetRequiredService(); - else if (routingSettings.Planner == "FeedbackReasoningPlanner") - return provider.GetRequiredService(); - throw new NotImplementedException(); }); services.AddScoped(); diff --git a/src/Infrastructure/BotSharp.Core/Planning/NaivePlanner.cs b/src/Infrastructure/BotSharp.Core/Planning/NaivePlanner.cs index c9aaf783..f1a8c74a 100644 --- a/src/Infrastructure/BotSharp.Core/Planning/NaivePlanner.cs +++ b/src/Infrastructure/BotSharp.Core/Planning/NaivePlanner.cs @@ -1,6 +1,7 @@ using BotSharp.Abstraction.Agents.Models; using BotSharp.Abstraction.Functions.Models; using BotSharp.Abstraction.Planning; +using BotSharp.Abstraction.Routing.Models; using BotSharp.Abstraction.Templating; namespace BotSharp.Core.Planning; @@ -16,7 +17,7 @@ public class NaivePlanner : IPlaner _logger = logger; } - public async Task GetNextInstruction(Agent router, string conversation) + public async Task GetNextInstruction(Agent router) { var next = GetNextStepPrompt(router); @@ -25,7 +26,7 @@ public class NaivePlanner : IPlaner var agentService = _services.GetRequiredService(); var instruction = agentService.RenderedInstruction(router); - var content = $"{instruction}\r\n{conversation}\r\n###\r\n{next}"; + var content = $"{instruction}\r\n###\r\n{next}"; // text completion content = content + "\r\nResponse: "; @@ -54,13 +55,22 @@ public class NaivePlanner : IPlaner retryCount++; } } - + + // Fix LLM malformed response + FixMalformedResponse(inst); + return inst; } + public async Task AgentExecuting(FunctionCallFromLlm inst, RoleDialogModel message) + { + return true; + } + public async Task AgentExecuted(FunctionCallFromLlm inst, RoleDialogModel message) { - inst.AgentName = null; + var context = _services.GetRequiredService(); + context.Empty(); return true; } @@ -73,4 +83,54 @@ public class NaivePlanner : IPlaner { }); } + + /// + /// Sometimes LLM hallucinates and fails to set function names correctly. + /// + /// + private void FixMalformedResponse(FunctionCallFromLlm args) + { + var agentService = _services.GetRequiredService(); + var agents = agentService.GetAgents(allowRouting: true).Result; + var malformed = false; + + // Sometimes it populate malformed Function in Agent name + if (!string.IsNullOrEmpty(args.Function) && + args.Function == args.AgentName) + { + args.Function = "route_to_agent"; + malformed = true; + } + + // Another case of malformed response + if (string.IsNullOrEmpty(args.AgentName) && + agents.Select(x => x.Name).Contains(args.Function)) + { + args.AgentName = args.Function; + args.Function = "route_to_agent"; + malformed = true; + } + + // It should be Route to agent, but it is used as Response to user. + if (!string.IsNullOrEmpty(args.AgentName) && + agents.Select(x => x.Name).Contains(args.AgentName) && + args.Function != "route_to_agent") + { + args.Function = "route_to_agent"; + malformed = true; + } + + // Function name shouldn't contain dot symbol + if (!string.IsNullOrEmpty(args.Function) && + args.Function.Contains('.')) + { + args.Function = args.Function.Split('.').Last(); + malformed = true; + } + + if (malformed) + { + _logger.LogWarning($"Captured LLM malformed response"); + } + } } diff --git a/src/Infrastructure/BotSharp.Core/Planning/FeedbackReasoningPlanner.cs b/src/Infrastructure/BotSharp.Core/Planning/ReasoningPlanner.cs similarity index 65% rename from src/Infrastructure/BotSharp.Core/Planning/FeedbackReasoningPlanner.cs rename to src/Infrastructure/BotSharp.Core/Planning/ReasoningPlanner.cs index 156cb590..2a4a5cac 100644 --- a/src/Infrastructure/BotSharp.Core/Planning/FeedbackReasoningPlanner.cs +++ b/src/Infrastructure/BotSharp.Core/Planning/ReasoningPlanner.cs @@ -1,30 +1,30 @@ using BotSharp.Abstraction.Agents.Models; using BotSharp.Abstraction.Functions.Models; using BotSharp.Abstraction.Planning; +using BotSharp.Abstraction.Repositories; +using BotSharp.Abstraction.Routing.Models; using BotSharp.Abstraction.Templating; namespace BotSharp.Core.Planning; -public class FeedbackReasoningPlanner : IPlaner +public class ReasoningPlanner : IPlaner { private readonly IServiceProvider _services; private readonly ILogger _logger; - public FeedbackReasoningPlanner(IServiceProvider services, ILogger logger) + public ReasoningPlanner(IServiceProvider services, ILogger logger) { _services = services; _logger = logger; } - public async Task GetNextInstruction(Agent router, string conversation) + public async Task GetNextInstruction(Agent router) { var next = GetNextStepPrompt(router); RoleDialogModel response = default; var inst = new FunctionCallFromLlm(); - var content = $"{conversation}\r\n###\r\n{next}"; - var completion = CompletionProvider.GetChatCompletion(_services, model: "llm-gpt4"); @@ -35,7 +35,7 @@ public class FeedbackReasoningPlanner : IPlaner { response = completion.GetChatCompletions(router, new List { - new RoleDialogModel(AgentRole.User, content) + new RoleDialogModel(AgentRole.User, next) }); inst = response.Content.JsonContent(); @@ -57,9 +57,28 @@ public class FeedbackReasoningPlanner : IPlaner return inst; } + public async Task AgentExecuting(FunctionCallFromLlm inst, RoleDialogModel message) + { + message.Content = inst.Question; + message.FunctionArgs = JsonSerializer.Serialize(inst.Arguments); + + var db = _services.GetRequiredService(); + var agent = db.GetAgents(inst.AgentName).FirstOrDefault(); + + var context = _services.GetRequiredService(); + context.Push(agent.Id); + + return true; + } + public async Task AgentExecuted(FunctionCallFromLlm inst, RoleDialogModel message) { - inst.AgentName = null; + var context = _services.GetRequiredService(); + context.Pop(); + + // push Router to continue + // Make decision according to last agent's response + return true; } diff --git a/src/Infrastructure/BotSharp.Core/Routing/Handlers/ContinueExecuteTaskRoutingHandler.cs b/src/Infrastructure/BotSharp.Core/Routing/Handlers/ContinueExecuteTaskRoutingHandler.cs index 10724208..815f5bc5 100644 --- a/src/Infrastructure/BotSharp.Core/Routing/Handlers/ContinueExecuteTaskRoutingHandler.cs +++ b/src/Infrastructure/BotSharp.Core/Routing/Handlers/ContinueExecuteTaskRoutingHandler.cs @@ -3,6 +3,7 @@ using BotSharp.Abstraction.Models; using BotSharp.Abstraction.Repositories; using BotSharp.Abstraction.Routing; using BotSharp.Abstraction.Routing.Settings; +using BotSharp.Core.Planning; namespace BotSharp.Core.Routing.Handlers; @@ -24,7 +25,7 @@ public class ContinueExecuteTaskRoutingHandler : RoutingHandlerBase, IRoutingHan public List Planers => new List { - "FeedbackReasoningPlanner" + nameof(ReasoningPlanner) }; public ContinueExecuteTaskRoutingHandler(IServiceProvider services, ILogger logger, RoutingSettings settings) diff --git a/src/Infrastructure/BotSharp.Core/Routing/Handlers/InterruptTaskExecutionRoutingHandler.cs b/src/Infrastructure/BotSharp.Core/Routing/Handlers/InterruptTaskExecutionRoutingHandler.cs index 9932777d..d1aeed35 100644 --- a/src/Infrastructure/BotSharp.Core/Routing/Handlers/InterruptTaskExecutionRoutingHandler.cs +++ b/src/Infrastructure/BotSharp.Core/Routing/Handlers/InterruptTaskExecutionRoutingHandler.cs @@ -1,6 +1,7 @@ using BotSharp.Abstraction.Functions.Models; using BotSharp.Abstraction.Routing; using BotSharp.Abstraction.Routing.Settings; +using BotSharp.Core.Planning; namespace BotSharp.Core.Routing.Handlers; @@ -18,7 +19,7 @@ public class InterruptTaskExecutionRoutingHandler : RoutingHandlerBase, IRouting public List Planers => new List { - "FeedbackReasoningPlanner" + nameof(ReasoningPlanner) }; public InterruptTaskExecutionRoutingHandler(IServiceProvider services, ILogger logger, RoutingSettings settings) diff --git a/src/Infrastructure/BotSharp.Core/Routing/Handlers/RetrieveDataFromAgentRoutingHandler.cs b/src/Infrastructure/BotSharp.Core/Routing/Handlers/RetrieveDataFromAgentRoutingHandler.cs index aa2cbce3..cf86dec7 100644 --- a/src/Infrastructure/BotSharp.Core/Routing/Handlers/RetrieveDataFromAgentRoutingHandler.cs +++ b/src/Infrastructure/BotSharp.Core/Routing/Handlers/RetrieveDataFromAgentRoutingHandler.cs @@ -1,10 +1,15 @@ using BotSharp.Abstraction.Functions.Models; using BotSharp.Abstraction.Repositories; using BotSharp.Abstraction.Routing; +using BotSharp.Abstraction.Routing.Models; using BotSharp.Abstraction.Routing.Settings; +using BotSharp.Core.Planning; namespace BotSharp.Core.Routing.Handlers; +/// +/// Retrieve information from specific agent +/// public class RetrieveDataFromAgentRoutingHandler : RoutingHandlerBase, IRoutingHandler { public string Name => "retrieve_data_from_agent"; @@ -13,9 +18,9 @@ public class RetrieveDataFromAgentRoutingHandler : RoutingHandlerBase, IRoutingH public List Parameters => new List { - new ParameterPropertyDef("agent", "the name of the agent"), - new ParameterPropertyDef("question", "the question you will ask the agent to get the necessary data"), new ParameterPropertyDef("reason", "why retrieve data"), + new ParameterPropertyDef("question", "the question you will ask the agent to get the necessary data"), + new ParameterPropertyDef("next_action_agent", "agent that can handle the question"), new ParameterPropertyDef("args", "required parameters extracted from question and hand over to the next agent") { Type = "object" @@ -24,7 +29,7 @@ public class RetrieveDataFromAgentRoutingHandler : RoutingHandlerBase, IRoutingH public List Planers => new List { - "FeedbackReasoningPlanner" + nameof(ReasoningPlanner) }; public RetrieveDataFromAgentRoutingHandler(IServiceProvider services, ILogger logger, RoutingSettings settings) @@ -34,33 +39,9 @@ public class RetrieveDataFromAgentRoutingHandler : RoutingHandlerBase, IRoutingH public async Task Handle(IRoutingService routing, FunctionCallFromLlm inst, RoleDialogModel message) { - // Retrieve information from specific agent - var db = _services.GetRequiredService(); - var record = db.GetAgents(inst.AgentName).FirstOrDefault(); - var ret = await routing.InvokeAgent(record.Id, message); + var context = _services.GetRequiredService(); + var ret = await routing.InvokeAgent(context.GetCurrentAgentId(), message); - /*_dialogs.Add(new RoleDialogModel(AgentRole.Assistant, inst.Parameters.Question) - { - CurrentAgentId = record.Id - });*/ - - _router.Instruction += $"\r\n{AgentRole.Assistant}: {inst.Question}"; - - /*_dialogs.Add(new RoleDialogModel(AgentRole.Function, inst.Parameters.Answer) - { - MessageId = inst.MessageId, - FunctionName = inst.Function, - FunctionArgs = JsonSerializer.Serialize(inst.Parameters.Arguments), - ExecutionResult = inst.Parameters.Answer, - ExecutionData = response.ExecutionData, - CurrentAgentId = record.Id - });*/ - - _router.Instruction += $"\r\n{AgentRole.Function}: {message.Content}"; - - // Got the response from agent, then send to reasoner again to make the decision - // inst = await GetNextInstructionFromReasoner($"What's the next step based on user's original goal and function result?"); - - return true; + return ret; } } diff --git a/src/Infrastructure/BotSharp.Core/Routing/Handlers/TaskEndRoutingHandler.cs b/src/Infrastructure/BotSharp.Core/Routing/Handlers/TaskEndRoutingHandler.cs index 656302e5..0dfdce94 100644 --- a/src/Infrastructure/BotSharp.Core/Routing/Handlers/TaskEndRoutingHandler.cs +++ b/src/Infrastructure/BotSharp.Core/Routing/Handlers/TaskEndRoutingHandler.cs @@ -1,6 +1,7 @@ using BotSharp.Abstraction.Functions.Models; using BotSharp.Abstraction.Routing; using BotSharp.Abstraction.Routing.Settings; +using BotSharp.Core.Planning; namespace BotSharp.Core.Routing.Handlers; @@ -17,7 +18,7 @@ public class TaskEndRoutingHandler : RoutingHandlerBase, IRoutingHandler public List Planers => new List { - "FeedbackReasoningPlanner" + nameof(ReasoningPlanner) }; public TaskEndRoutingHandler(IServiceProvider services, ILogger logger, RoutingSettings settings) diff --git a/src/Infrastructure/BotSharp.Core/Routing/RoutingService.FixMalformedResponse.cs b/src/Infrastructure/BotSharp.Core/Routing/RoutingService.FixMalformedResponse.cs deleted file mode 100644 index 1818924c..00000000 --- a/src/Infrastructure/BotSharp.Core/Routing/RoutingService.FixMalformedResponse.cs +++ /dev/null @@ -1,56 +0,0 @@ -using BotSharp.Abstraction.Functions.Models; - -namespace BotSharp.Core.Routing; - -public partial class RoutingService -{ - /// - /// Sometimes LLM hallucinates and fails to set function names correctly. - /// - /// - private void FixMalformedResponse(FunctionCallFromLlm args) - { - var agentService = _services.GetRequiredService(); - var agents = agentService.GetAgents(allowRouting: true).Result; - var malformed = false; - - // Sometimes it populate malformed Function in Agent name - if (!string.IsNullOrEmpty(args.Function) && - args.Function == args.AgentName) - { - args.Function = "route_to_agent"; - malformed = true; - } - - // Another case of malformed response - if (string.IsNullOrEmpty(args.AgentName) && - agents.Select(x => x.Name).Contains(args.Function)) - { - args.AgentName = args.Function; - args.Function = "route_to_agent"; - malformed = true; - } - - // It should be Route to agent, but it is used as Response to user. - if (!string.IsNullOrEmpty(args.AgentName) && - agents.Select(x => x.Name).Contains(args.AgentName) && - args.Function != "route_to_agent") - { - args.Function = "route_to_agent"; - malformed = true; - } - - // Function name shouldn't contain dot symbol - if (!string.IsNullOrEmpty(args.Function) && - args.Function.Contains('.')) - { - args.Function = args.Function.Split('.').Last(); - malformed = true; - } - - if (malformed) - { - _logger.LogWarning($"Captured LLM malformed response"); - } - } -} diff --git a/src/Infrastructure/BotSharp.Core/Routing/RoutingService.InvokeAgent.cs b/src/Infrastructure/BotSharp.Core/Routing/RoutingService.InvokeAgent.cs index f19bee3f..7946dca6 100644 --- a/src/Infrastructure/BotSharp.Core/Routing/RoutingService.InvokeAgent.cs +++ b/src/Infrastructure/BotSharp.Core/Routing/RoutingService.InvokeAgent.cs @@ -65,7 +65,7 @@ public partial class RoutingService } else { - await InvokeAgent(message.CurrentAgentId, message); + await InvokeAgent(agent.Id, message); } } else diff --git a/src/Infrastructure/BotSharp.Core/Routing/RoutingService.cs b/src/Infrastructure/BotSharp.Core/Routing/RoutingService.cs index 25d4732b..98e4837a 100644 --- a/src/Infrastructure/BotSharp.Core/Routing/RoutingService.cs +++ b/src/Infrastructure/BotSharp.Core/Routing/RoutingService.cs @@ -2,6 +2,7 @@ using BotSharp.Abstraction.Agents.Models; using BotSharp.Abstraction.Functions.Models; using BotSharp.Abstraction.Planning; using BotSharp.Abstraction.Routing; +using BotSharp.Abstraction.Routing.Models; using BotSharp.Abstraction.Routing.Settings; using System.Drawing; @@ -71,6 +72,7 @@ public partial class RoutingService : IRoutingService _routerInstance.Load(); var router = _routerInstance.Router; + var context = _services.GetRequiredService(); var planner = _services.GetRequiredService(); var executor = _services.GetRequiredService(); @@ -81,12 +83,10 @@ public partial class RoutingService : IRoutingService loopCount++; var conversation = await GetConversationContent(Dialogs); + router.TemplateDict["conversation"] = conversation; // Get instruction from Planner - var inst = await planner.GetNextInstruction(router, conversation); - - // Fix LLM malformed response - FixMalformedResponse(inst); + var inst = await planner.GetNextInstruction(router); // Save states SaveStateByArgs(inst.Arguments); @@ -96,6 +96,7 @@ public partial class RoutingService : IRoutingService #else _logger.LogInformation($"*** Next Instruction *** {inst}"); #endif + await planner.AgentExecuting(inst, message); // Handle instruction by Executor var executed = await executor.Execute(this, router, inst, Dialogs, message); @@ -103,7 +104,7 @@ public partial class RoutingService : IRoutingService await planner.AgentExecuted(inst, message); // There is no need for the agent to continue processing, indicating that the task has been completed. - if (inst.AgentName == null) + if (context.IsEmpty || context.GetCurrentAgentId() == router.Id) { break; } diff --git a/src/Plugins/BotSharp.Plugin.AzureOpenAI/Providers/ChatCompletionProvider.cs b/src/Plugins/BotSharp.Plugin.AzureOpenAI/Providers/ChatCompletionProvider.cs index 651c2906..1b0b2c6e 100644 --- a/src/Plugins/BotSharp.Plugin.AzureOpenAI/Providers/ChatCompletionProvider.cs +++ b/src/Plugins/BotSharp.Plugin.AzureOpenAI/Providers/ChatCompletionProvider.cs @@ -58,8 +58,6 @@ public class ChatCompletionProvider : IChatCompletion if (choice.FinishReason == CompletionsFinishReason.FunctionCall) { - _logger.LogInformation($"[{agent.Name}]: {message.FunctionCall.Name}({message.FunctionCall.Arguments})"); - msg = new RoleDialogModel(AgentRole.Function, message.Content) { CurrentAgentId = agent.Id, @@ -77,7 +75,9 @@ public class ChatCompletionProvider : IChatCompletion var setting = _services.GetRequiredService(); if (setting.ShowVerboseLog) { - _logger.LogInformation(msg.Content); + _logger.LogInformation(msg.Role == AgentRole.Function ? + $"[{agent.Name}]: {msg.FunctionName}({msg.FunctionArgs})" : + $"[{agent.Name}]: {msg.Content}"); } // After chat completion hook diff --git a/src/Plugins/BotSharp.Plugin.AzureOpenAI/Providers/ProviderHelper.cs b/src/Plugins/BotSharp.Plugin.AzureOpenAI/Providers/ProviderHelper.cs index caaa520f..2be17f0a 100644 --- a/src/Plugins/BotSharp.Plugin.AzureOpenAI/Providers/ProviderHelper.cs +++ b/src/Plugins/BotSharp.Plugin.AzureOpenAI/Providers/ProviderHelper.cs @@ -11,7 +11,7 @@ public class ProviderHelper { public static OpenAIClient GetClient(string model, AzureOpenAiSettings settings) { - if (model == "gpt-4" || model == "llm-gpt4") + if (model.Contains("gpt-4") || model.Contains("gpt4")) { var client = new OpenAIClient(new Uri(settings.GPT4.Endpoint), new AzureKeyCredential(settings.GPT4.ApiKey)); return client; diff --git a/src/WebStarter/data/agents/01fcc3e5-9af7-49e6-ad7a-a760bd12dc4a/instruction.liquid b/src/WebStarter/data/agents/01fcc3e5-9af7-49e6-ad7a-a760bd12dc4a/instruction.liquid index 7b4d552d..b0e0e698 100644 --- a/src/WebStarter/data/agents/01fcc3e5-9af7-49e6-ad7a-a760bd12dc4a/instruction.liquid +++ b/src/WebStarter/data/agents/01fcc3e5-9af7-49e6-ad7a-a760bd12dc4a/instruction.liquid @@ -29,4 +29,5 @@ Arguments: {%- endif %} {% endfor %} -[CONVERSATION] \ No newline at end of file +[CONVERSATION] +{{ conversation }} \ No newline at end of file