LangChain4j之HelloWorld
什么是LangChain4j
它是Java版本的LangChain,随着大模型的不断发展,如何在程序中更好的利用大模型的能力来提高编程效率是一种趋势,LangChain是这么自己介绍自己的:
LangChain gives developers a framework to construct LLM‑powered apps easily.
意思是:LangChain提供了一个开发框架,使得开发者可以很容易的用来构建具有LLM能力的应用程序。
LLM就是Large Language Model,也就是常说的大语言模型,简称大模型。
个人认为:大模型时代,如何将大模型能力和传统应用相结合,使得传统应用更加智能,是人工智能时代的趋势。以前一个应用要获得智能,需要企业自己投入资源训练模型,而现在只需要接入大模型即可,这种便利性将使得大模型会应用得更为广泛,而如何将大模型能力和Java编程语言相结合,这就是LangChain4j所做的。
注意,大模型的能力远远不止聊天的能力,而LangChain4j就在帮助我们更好的利用大模型的能力,从而帮我们打造出更加智能的应用。
初识LangChain4j
接下来,让我们与LangChain4j初识一下,新建一个Maven工程,然后添加以下依赖:
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd"><modelVersion>4.0.0</modelVersion><groupId>com.timi</groupId><artifactId>langchain4j-demo</artifactId><version>1.0-SNAPSHOT</version><properties><maven.compiler.source>17</maven.compiler.source><maven.compiler.target>17</maven.compiler.target><project.build.sourceEncoding>UTF-8</project.build.sourceEncoding><langchain4j.version>0.27.1</langchain4j.version></properties><dependencies><dependency><groupId>dev.langchain4j</groupId><artifactId>langchain4j</artifactId><version>${langchain4j.version}</version></dependency><dependency><groupId>dev.langchain4j</groupId><artifactId>langchain4j-open-ai</artifactId><version>${langchain4j.version}</version></dependency><dependency><groupId>org.tinylog</groupId><artifactId>tinylog-impl</artifactId><version>2.6.2</version></dependency><dependency><groupId>org.tinylog</groupId><artifactId>slf4j-tinylog</artifactId><version>2.6.2</version></dependency></dependencies></project>
引入了langchain4j的核心依赖、langchain4j集成OpenAi各个模型的依赖、轻量级实现了slf4j接口的tinylog日志依赖。
和OpenAi的第一次对话
package com.timi;import dev.langchain4j.model.chat.ChatLanguageModel;
import dev.langchain4j.model.openai.OpenAiChatModel;public class _01_HelloWorld {public static void main(String[] args) {ChatLanguageModel model = OpenAiChatModel.withApiKey("demo");String answer = model.generate("你好,你是谁?");System.out.println(answer);}
}
运行代码结果为:
你好,我是一个人工智能助手。我可以回答你的问题和提供帮助。有什么可以帮到你的吗?
这样,我们使用LangChain4j第一次成功的和OpenAi的GPT模型进行了对话,正常来说,调用OpenAi的API接口需要在OpenAi的官网去申请ApiKey才能调用成功,而我这里传入的ApiKey为"demo"却也能调通,这是因为:
public OpenAiChatModel(String baseUrl, String apiKey, String organizationId, String modelName, Double temperature, Double topP, List<String> stop, Integer maxTokens, Double presencePenalty, Double frequencyPenalty, Map<String, Integer> logitBias, String responseFormat, Integer seed, String user, Duration timeout, Integer maxRetries, Proxy proxy, Boolean logRequests, Boolean logResponses, Tokenizer tokenizer) {baseUrl = (String)Utils.getOrDefault(baseUrl, "https://api.openai.com/v1");if ("demo".equals(apiKey)) {baseUrl = "http://langchain4j.dev/demo/openai/v1";}//其他代码
}
在底层在构造OpenAiChatModel时,会判断传入的ApiKey是否等于"demo",如果等于会将OpenAi的原始API地址"https://api.openai.com/v1"改为"http://langchain4j.dev/demo/openai/v1",这个地址是langchain4j专门为我们准备的一个体验地址,实际上这个地址相当于是"https://api.openai.com/v1"的代理,我们请求代理时,代理会去调用真正的OpenAi接口,只不过代理会将自己的ApiKey传过去,从而拿到结果返回给我们。
所以,真正开发时,需要大家设置自己的apiKey或baseUrl,可以这么设置:
ChatLanguageModel model = OpenAiChatModel.builder().baseUrl("http://langchain4j.dev/demo/openai/v1").apiKey("demo").build();
多轮对话
前面的例子中,我们通过ChatLanguageModel的generate()方法向大模型提出问题:
String answer = model.generate("你好,你是谁?");
那如果我继续向大模型说:
model.generate("请重复")
那么大模型还记得它之前的回答吗?我们先看看效果,代码如下:
package com.timi;import dev.langchain4j.model.chat.ChatLanguageModel;
import dev.langchain4j.model.openai.OpenAiChatModel;public class _01_HelloWorld {public static void main(String[] args) {ChatLanguageModel model = OpenAiChatModel.builder().baseUrl("http://langchain4j.dev/demo/openai/v1").apiKey("demo").build();System.out.println(model.generate("你好,你是谁?"));System.out.println("----");System.out.println(model.generate("请重复"));}
}
运行结果如下:
你好,我是一个聊天机器人,可以回答你的问题和进行对话。有什么可以帮助你的吗?
----
请重复
大模型重复了我第二次跟它说的,而不是重复它的第一次回答,这是因为,目前的代码中,每次调用generate()都是一次新的会话,我再举一个例子:
package com.timi;import dev.langchain4j.model.chat.ChatLanguageModel;
import dev.langchain4j.model.openai.OpenAiChatModel;public class _01_HelloWorld {public static void main(String[] args) {ChatLanguageModel model = OpenAiChatModel.builder().baseUrl("http://langchain4j.dev/demo/openai/v1").apiKey("demo").build();System.out.println(model.generate("你好,我是Timi"));System.out.println("----");System.out.println(model.generate("我叫什么"));}
}
运行结果为:
你好Timi,有什么可以帮助你的吗?
----
抱歉,我不知道您的名字。您可以告诉我您的名字吗?我可以记住它并以后称呼您。
一样的情况,因为第二次调用generate()方法是一次单独的会话,那么如何做到使得两次或多次generate()在同一个会话中呢?在LangChain4j中有一个ChatMemory组件,它就是专门用来实现会话功能的,但是它需要结合LangChain4j中的AiService来使用,我们后面再介绍,现在我们先使用笨办法来解决多轮对话的问题。
在ChatLanguageModel中有多个generate()重载方法:
default String generate(String userMessage) {return generate(UserMessage.from(userMessage)).content().text();
}default Response<AiMessage> generate(ChatMessage... messages) {return generate(asList(messages));
}Response<AiMessage> generate(List<ChatMessage> messages);
我们前面使用的就是第一个generate()方法,而第二个和第三个generate()方法都是接收一个ChatMessage集合,并且返回一个AiMessage,那么ChatMessage和AiMessage分别都表示什么意思呢?
ChatMessage是一个接口,表示聊天消息,它有以下四种实现:
- UserMessage:表示用户发送给大模型的消息
- AiMessage:表示大模型响应给用户的消息
- SystemMessage:也是用户发送给大模型的消息,和UserMessage不同在于,SystemMessage一般是应用程序帮用户设置的,举个例子,假如有一个作家应用,那么“请你扮演一名作家,请帮我写一篇关于春天的作文”,其中“请你扮演一名作家”就是SystemMessage,“请帮我写一篇关于春天的作文”就是UserMessage
- ToolExecutionResultMessage:也是用户发送给大模型的,表示工具的执行结果,关于LangChain4j的工具机制,会在后续介绍,目前可以忽略
我们先重点关注UserMessage和AiMessage,它们就相当于请求和响应,所以如果我们想要实现多轮对话,可以这么实现:
package com.timi;import dev.langchain4j.data.message.AiMessage;
import dev.langchain4j.data.message.UserMessage;
import dev.langchain4j.memory.ChatMemory;
import dev.langchain4j.memory.chat.MessageWindowChatMemory;
import dev.langchain4j.model.chat.ChatLanguageModel;
import dev.langchain4j.model.openai.OpenAiChatModel;
import dev.langchain4j.model.output.Response;public class _01_HelloWorld {public static void main(String[] args) {ChatLanguageModel model = OpenAiChatModel.builder().baseUrl("http://langchain4j.dev/demo/openai/v1").apiKey("demo").build();UserMessage userMessage1 = UserMessage.userMessage("你好,我是Timi");Response<AiMessage> response1 = model.generate(userMessage1);AiMessage aiMessage1 = response1.content(); // 大模型的第一次响应System.out.println(aiMessage1.text());System.out.println("----");// 下面一行代码是重点Response<AiMessage> response2 = model.generate(userMessage1, aiMessage1, UserMessage.userMessage("我叫什么"));AiMessage aiMessage2 = response2.content(); // 大模型的第二次响应System.out.println(aiMessage2.text());}
}
代码执行结果为:
你好,Timi。有什么可以帮助你的吗?
----
您的名字是Timi。有什么其他问题我可以帮忙解答吗?
其中重点代码为:
Response<AiMessage> response2 = model.generate(userMessage1, aiMessage1, UserMessage.userMessage("我叫什么"));
同样是问"我叫什么",但是这里我把第一次的问题和答案,也就是我和大模型的历史对话传给了大模型,只有这样,大模型才能结合历史对话知道"我叫什么"。事实上,我们在使用ChatGPT时也是一样的原理,因为ChatGPT需要结合历史对话才更能理解你最新一句话的真正意思。
打字机流式响应
在前面的例子中,当我们通过ChatLanguageModel的generate()方法向大模型提问时,ChatLanguageModel一次性给了整段响应结果,而不是一个字一个字打字机式的回答,不过我们可以使用OpenAiStreamingChatModel来实现打字机效果,代码如下:
package com.timi;import dev.langchain4j.data.message.AiMessage;
import dev.langchain4j.memory.ChatMemory;
import dev.langchain4j.memory.chat.MessageWindowChatMemory;
import dev.langchain4j.model.StreamingResponseHandler;
import dev.langchain4j.model.chat.ChatLanguageModel;
import dev.langchain4j.model.chat.StreamingChatLanguageModel;
import dev.langchain4j.model.openai.OpenAiChatModel;
import dev.langchain4j.model.openai.OpenAiStreamingChatModel;import java.util.concurrent.TimeUnit;public class _01_HelloWorld {public static void main(String[] args) {StreamingChatLanguageModel model = OpenAiStreamingChatModel.builder().baseUrl("http://langchain4j.dev/demo/openai/v1").apiKey("demo").build();model.generate("你好,你是谁?", new StreamingResponseHandler<AiMessage>() {@Overridepublic void onNext(String token) {System.out.println(token);try {TimeUnit.SECONDS.sleep(1);} catch (InterruptedException e) {throw new RuntimeException(e);}}@Overridepublic void onError(Throwable error) {System.out.println(error);}});}
}
这样就能实现打字机效果了。
整合SpringBoot
先引入SpringBoot:
<parent><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-parent</artifactId><version>3.2.1</version>
</parent>
<dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-web</artifactId>
</dependency><dependency><groupId>dev.langchain4j</groupId><artifactId>langchain4j-open-ai-spring-boot-starter</artifactId><version>0.27.1</version>
</dependency>
然后定义SpringBoot启动类:
package com.timi;import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;@SpringBootApplication
public class Main {public static void main(String[] args) {SpringApplication.run(Main.class, args);}
}
然后定义HelloController:
package com.timi;import dev.langchain4j.model.chat.ChatLanguageModel;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;@RestController
public class HelloController {@Autowiredprivate ChatLanguageModel chatLanguageModel;@GetMapping("/hello")public String hello(){return chatLanguageModel.generate("你好啊");}
}
配置api-key:
langchain4j.open-ai.chat-model.api-key=demo
启动SpringBoot并访问:
ModerationModel
package com.timi;import dev.langchain4j.model.chat.ChatLanguageModel;
import dev.langchain4j.model.moderation.Moderation;
import dev.langchain4j.model.moderation.ModerationModel;
import dev.langchain4j.model.openai.OpenAiChatModel;
import dev.langchain4j.model.openai.OpenAiModerationModel;
import dev.langchain4j.model.output.Response;public class _01_HelloWorld {public static void main(String[] args) {ModerationModel moderationModel = OpenAiModerationModel.withApiKey("demo");Response<Moderation> response = moderationModel.moderate("我要谢谢你");System.out.println(response.content().flaggedText());}
}
ModerationModel能够校验输入中是否存在敏感内容。
ImageModel
package com.timi;import dev.langchain4j.data.image.Image;
import dev.langchain4j.model.chat.ChatLanguageModel;
import dev.langchain4j.model.image.ImageModel;
import dev.langchain4j.model.moderation.Moderation;
import dev.langchain4j.model.moderation.ModerationModel;
import dev.langchain4j.model.openai.OpenAiChatModel;
import dev.langchain4j.model.openai.OpenAiImageModel;
import dev.langchain4j.model.openai.OpenAiModerationModel;
import dev.langchain4j.model.output.Response;public class _01_HelloWorld {public static void main(String[] args) {ImageModel imageModel = OpenAiImageModel.builder().baseUrl("http://localhost:3000/v1").apiKey("sk-peszVtFXoLnWK45bB15370Df6f344cAa9a088eF50f9c7302").build();Response<Image> response = imageModel.generate("一辆车");System.out.println(response.content().url());}
}
ImageModel可以根据提示词来生成图片,默认提供的“demo”key不能用来生成图片,需要大家自己购买apiKey,可以先不买,先学习后面内容。
本节总结
本节我们介绍了LangChain4j的基本使用,以及多轮对话、流式响应的实现,其中我们提到了LangChain4j中的工具机制、AiService机制、ChatMemory机制,接下来我们先学习一下AiService机制。
相关文章:
LangChain4j之HelloWorld
什么是LangChain4j 它是Java版本的LangChain,随着大模型的不断发展,如何在程序中更好的利用大模型的能力来提高编程效率是一种趋势,LangChain是这么自己介绍自己的: LangChain gives developers a framework to construct LLM‑p…...
MySQL 基础概念
MySQL逻辑架构 MySQL 服务器逻辑架构图 最上层的服务并不是MySQL所独有的,大多数基于网络的客户端/服务器的工具或者服务都有类似的架构,比如连接管理、授权认证、安全等等。 大多数MySQL的核心服务都在第二层,包括查询解析、分析、优化、…...
RabbitMQ 学习笔记
RabbitMQ学习笔记 一些概念 Broker :RabbitMQ服务。 virtual host: 其实就是分组。 Connection:连接,生产者消费者与Broker之间的TCP连接。 Channel:网络信道,轻量级的Connection,使用Chann…...
【区分vue2和vue3下的element UI MessageBox 弹框组件,分别详细介绍属性,事件,方法如何使用,并举例】
在 Vue 2 中,Element UI 提供了 MessageBox 弹框组件,用于显示消息提示、确认消息和获取用户输入等。而在 Vue 3 的 Element Plus 中,虽然组件和 API 可能有所变化,但基本概念和用法是相似的。下面我将分别介绍 Vue 2 的 Element …...
避而不见!BigDecimal的四大坑
BigDecimal概述 定义:Java中的类,用于表示任意精度的十进制数。适用场景:需要高精度计算的场合,如金融、货币、税收等。 一、浮点精度的坑 问题:使用BigDecimal的equals和compareTo方法比较数值时,存在精…...
IDEA 安装与激活详细教程最新(附最新激活码)2099年亲测有效!
我们先从 IDEA 官网下载 IDEA 2024.1 版本的安装包,下载链接如下: https://www.jetbrains.com/idea/download/ 点击下载(下载Ultimate版),静心等待其下载完毕即可。 激活方式: 正版专属激活码领取...
LeetCode 100334. 包含所有 1 的最小矩形面积 I
更多题解尽在 https://sugar.matrixlab.dev/algorithm 每日更新。 组队打卡,更多解法等你一起来参与哦! LeetCode 100334. 包含所有 1 的最小矩形面积 I,难度中等。 遍历 解题思路:去掉矩形上下左右全为 0 的行和列 class Solu…...
pdf只要前几页,pdf怎么只要前几页
在现代办公和学习环境中,PDF文件已成为我们日常处理信息的重要工具。然而,有时我们并不需要整个PDF文件的内容,而只是其中的几页。那么,如何高效地提取PDF文件中的特定页面呢?本文将为您介绍几种实用的方法。 打开 “ …...
JAVA JVM 是怎么判定对象已经“死去”?
Java虚拟机(JVM)使用垃圾收集(Garbage Collection,GC)机制来自动管理内存,其中包括识别和回收不再使用的对象。JVM判定对象已经“死去”(即不再被任何引用所指向)的过程主要基于以下…...
springboot加载注入bean的方式
在SpringBoot的大环境下,基本上很少使用之前的xml配置Bean,主要是因为这种方式不好维护而且也不够方便。 springboto注入bean主要采用下图几种方式,分为本地服务工程注解声明的bean和外部依赖包中的bean。 一、 springboot装配本地服务工程…...
PostgreSQL 数据库设计与管理(四)
1. 数据库设计原则 1.1 规范化 规范化是组织数据库结构的一种方法,旨在减少数据冗余并提高数据完整性。常用的规范化范式包括: 第一范式(1NF): 确保每列都是原子的,不可再分。第二范式(2NF&a…...
Studying-代码随想录训练营day21| 669.修建二叉搜索树、108.将有序数组转换为二叉搜索树、538.把二叉搜索树转换为累加树、二叉树总结
第21天,二叉树最后一篇,冲💪 目录 669.修建二叉搜索树 108.将有序数组转换为二叉搜索树 538.把二叉搜索树转换为累加树 二叉树总结 669.修建二叉搜索树 文档讲解:代码随想录修建二叉搜索树 视频讲解:手撕修建二叉…...
GraphQL:简介
GraphQL 图片来源: 我们将探索GraphQL 的基础知识,并学习如何使用Apollo将其与 React 和 React Native 等前端框架连接起来。这将帮助您了解如何使用 GraphQL、React、React Native 和 Apollo 构建现代、高效的应用程序。 什么是 GraphQL?…...
AI大模型安全挑战和安全要求解读
引言 随着人工智能技术的飞速发展,大模型技术以其卓越的性能和广泛的应用前景,正在重塑人工智能领域的新格局。然而,任何技术都有两面性,大模型在带来前所未有便利的同时,也引发了深刻的安全和伦理挑战。 大模型&…...
前端面试题-token的存放位置
哈喽小伙伴们大家好,本系列是一个专门针对前端开发岗的面试题系列,每周将会不定期分享一些面试题,希望对大家有所帮助. 面试官:token 一般在客户端存在哪儿 求职者:Token一般在客户端存在以下几个地方: (1)Cookie:Token可以存储在客户端的Cookie中。服…...
深入探讨计算机网络中的各种报文
在计算机网络中,报文(Packet)是数据传输的基本单位。不同的协议使用不同类型的报文来实现数据传输的各种功能。本文将详细探讨计算机网络中常见的几种报文类型,并通过举例说明其具体应用。 一、TCP/IP协议栈中的报文 TCP/IP协议…...
Debezium系列之:Mysql和SQLServer数据库字段类型覆盖测试
Debezium系列之:Mysql和SQLServer数据库字段类型覆盖测试 一、需求背景二、类型对比三、完整流程三、Mysql数据库全字段类型覆盖测试四、SQLServer数据库字段类型覆盖测试一、需求背景 Debezium版本升级迭代,要做字段类型测试,确保版本间字段类型的差异下游能够自动适应,或…...
Mathtype7在Word2016中闪退(安装过6)
安装教程:https://blog.csdn.net/Little_pudding10/article/details/135465291 Mathtype7在Word2016中闪退是因为安装过Mathtype6,MathPage.wll和MathType Comm***.dotm),不会随着Mathtype的删除自动删除,而新版的Mathtype中的文件…...
SQL面试题练习 —— 合并用户浏览行为
目录 1 题目2 建表语句3 题解 1 题目 有一份用户访问记录表,记录用户id和访问时间,如果用户访问时间间隔小于60s则认为时一次浏览,请合并用户的浏览行为。 样例数据 ------------------------ | user_id | access_time | ---------------…...
【Docker】docker 替换宿主与容器的映射端口和文件路径
every blog every motto: You can do more than you think. https://blog.csdn.net/weixin_39190382?typeblog 0. 前言 docker 替换宿主与容器的映射端口和文件夹 1. 正文 1.1 关闭docker 服务 systemctl stop docker1.2 找到容器的配置文件 cd /var/lib/docker/contain…...
未来机器人的大脑:如何用神经网络模拟器实现更智能的决策?
编辑:陈萍萍的公主一点人工一点智能 未来机器人的大脑:如何用神经网络模拟器实现更智能的决策?RWM通过双自回归机制有效解决了复合误差、部分可观测性和随机动力学等关键挑战,在不依赖领域特定归纳偏见的条件下实现了卓越的预测准…...
React hook之useRef
React useRef 详解 useRef 是 React 提供的一个 Hook,用于在函数组件中创建可变的引用对象。它在 React 开发中有多种重要用途,下面我将全面详细地介绍它的特性和用法。 基本概念 1. 创建 ref const refContainer useRef(initialValue);initialValu…...
【人工智能】神经网络的优化器optimizer(二):Adagrad自适应学习率优化器
一.自适应梯度算法Adagrad概述 Adagrad(Adaptive Gradient Algorithm)是一种自适应学习率的优化算法,由Duchi等人在2011年提出。其核心思想是针对不同参数自动调整学习率,适合处理稀疏数据和不同参数梯度差异较大的场景。Adagrad通…...
鸿蒙中用HarmonyOS SDK应用服务 HarmonyOS5开发一个医院挂号小程序
一、开发准备 环境搭建: 安装DevEco Studio 3.0或更高版本配置HarmonyOS SDK申请开发者账号 项目创建: File > New > Create Project > Application (选择"Empty Ability") 二、核心功能实现 1. 医院科室展示 /…...
【项目实战】通过多模态+LangGraph实现PPT生成助手
PPT自动生成系统 基于LangGraph的PPT自动生成系统,可以将Markdown文档自动转换为PPT演示文稿。 功能特点 Markdown解析:自动解析Markdown文档结构PPT模板分析:分析PPT模板的布局和风格智能布局决策:匹配内容与合适的PPT布局自动…...
Cloudflare 从 Nginx 到 Pingora:性能、效率与安全的全面升级
在互联网的快速发展中,高性能、高效率和高安全性的网络服务成为了各大互联网基础设施提供商的核心追求。Cloudflare 作为全球领先的互联网安全和基础设施公司,近期做出了一个重大技术决策:弃用长期使用的 Nginx,转而采用其内部开发…...
C++.OpenGL (10/64)基础光照(Basic Lighting)
基础光照(Basic Lighting) 冯氏光照模型(Phong Lighting Model) #mermaid-svg-GLdskXwWINxNGHso {font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}#mermaid-svg-GLdskXwWINxNGHso .error-icon{fill:#552222;}#mermaid-svg-GLd…...
鸿蒙DevEco Studio HarmonyOS 5跑酷小游戏实现指南
1. 项目概述 本跑酷小游戏基于鸿蒙HarmonyOS 5开发,使用DevEco Studio作为开发工具,采用Java语言实现,包含角色控制、障碍物生成和分数计算系统。 2. 项目结构 /src/main/java/com/example/runner/├── MainAbilitySlice.java // 主界…...
嵌入式学习笔记DAY33(网络编程——TCP)
一、网络架构 C/S (client/server 客户端/服务器):由客户端和服务器端两个部分组成。客户端通常是用户使用的应用程序,负责提供用户界面和交互逻辑 ,接收用户输入,向服务器发送请求,并展示服务…...
使用Spring AI和MCP协议构建图片搜索服务
目录 使用Spring AI和MCP协议构建图片搜索服务 引言 技术栈概览 项目架构设计 架构图 服务端开发 1. 创建Spring Boot项目 2. 实现图片搜索工具 3. 配置传输模式 Stdio模式(本地调用) SSE模式(远程调用) 4. 注册工具提…...
