contentgrid-assistant-api 0.1.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- contentgrid_assistant_api-0.1.0/LICENSE +13 -0
- contentgrid_assistant_api-0.1.0/PKG-INFO +401 -0
- contentgrid_assistant_api-0.1.0/README.md +358 -0
- contentgrid_assistant_api-0.1.0/pyproject.toml +43 -0
- contentgrid_assistant_api-0.1.0/setup.cfg +4 -0
- contentgrid_assistant_api-0.1.0/setup.py +3 -0
- contentgrid_assistant_api-0.1.0/src/contentgrid_assistant_api/app.py +131 -0
- contentgrid_assistant_api-0.1.0/src/contentgrid_assistant_api/config.py +55 -0
- contentgrid_assistant_api-0.1.0/src/contentgrid_assistant_api/db/repositories/thread_repository.py +78 -0
- contentgrid_assistant_api-0.1.0/src/contentgrid_assistant_api/db/types/message.py +25 -0
- contentgrid_assistant_api-0.1.0/src/contentgrid_assistant_api/db/types/thread.py +46 -0
- contentgrid_assistant_api-0.1.0/src/contentgrid_assistant_api/dependencies.py +79 -0
- contentgrid_assistant_api-0.1.0/src/contentgrid_assistant_api/routers/agent_home.py +41 -0
- contentgrid_assistant_api-0.1.0/src/contentgrid_assistant_api/routers/message_router.py +256 -0
- contentgrid_assistant_api-0.1.0/src/contentgrid_assistant_api/routers/thread_router.py +129 -0
- contentgrid_assistant_api-0.1.0/src/contentgrid_assistant_api/types/agents.py +50 -0
- contentgrid_assistant_api-0.1.0/src/contentgrid_assistant_api/types/context.py +9 -0
- contentgrid_assistant_api-0.1.0/src/contentgrid_assistant_api.egg-info/PKG-INFO +401 -0
- contentgrid_assistant_api-0.1.0/src/contentgrid_assistant_api.egg-info/SOURCES.txt +22 -0
- contentgrid_assistant_api-0.1.0/src/contentgrid_assistant_api.egg-info/dependency_links.txt +1 -0
- contentgrid_assistant_api-0.1.0/src/contentgrid_assistant_api.egg-info/requires.txt +17 -0
- contentgrid_assistant_api-0.1.0/src/contentgrid_assistant_api.egg-info/top_level.txt +1 -0
- contentgrid_assistant_api-0.1.0/tests/test_thread_access_control.py +427 -0
- contentgrid_assistant_api-0.1.0/tests/test_tools_endpoint.py +317 -0
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
Copyright 2024 Xenit Solutions
|
|
2
|
+
|
|
3
|
+
Licensed under the Apache License, Version 2.0 (the "License");
|
|
4
|
+
you may not use this file except in compliance with the License.
|
|
5
|
+
You may obtain a copy of the License at
|
|
6
|
+
|
|
7
|
+
http://www.apache.org/licenses/LICENSE-2.0
|
|
8
|
+
|
|
9
|
+
Unless required by applicable law or agreed to in writing, software
|
|
10
|
+
distributed under the License is distributed on an "AS IS" BASIS,
|
|
11
|
+
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
12
|
+
See the License for the specific language governing permissions and
|
|
13
|
+
limitations under the License.
|
|
@@ -0,0 +1,401 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: contentgrid-assistant-api
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: FastAPI wrapper for contentgrid assistants
|
|
5
|
+
Author-email: Ranec Belpaire <ranec.belpaire@xenit.eu>
|
|
6
|
+
License: Copyright 2024 Xenit Solutions
|
|
7
|
+
|
|
8
|
+
Licensed under the Apache License, Version 2.0 (the "License");
|
|
9
|
+
you may not use this file except in compliance with the License.
|
|
10
|
+
You may obtain a copy of the License at
|
|
11
|
+
|
|
12
|
+
http://www.apache.org/licenses/LICENSE-2.0
|
|
13
|
+
|
|
14
|
+
Unless required by applicable law or agreed to in writing, software
|
|
15
|
+
distributed under the License is distributed on an "AS IS" BASIS,
|
|
16
|
+
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
17
|
+
See the License for the specific language governing permissions and
|
|
18
|
+
limitations under the License.
|
|
19
|
+
Classifier: Development Status :: 3 - Alpha
|
|
20
|
+
Classifier: Operating System :: OS Independent
|
|
21
|
+
Classifier: Programming Language :: Python :: 3
|
|
22
|
+
Requires-Python: >=3.5
|
|
23
|
+
Description-Content-Type: text/markdown
|
|
24
|
+
License-File: LICENSE
|
|
25
|
+
Requires-Dist: fastapi>=0.124.0
|
|
26
|
+
Requires-Dist: openai>=2.7.2
|
|
27
|
+
Requires-Dist: pydantic<3,>=2.11.9
|
|
28
|
+
Requires-Dist: pydantic-settings>=2.12.0
|
|
29
|
+
Requires-Dist: langchain>=1.2.3
|
|
30
|
+
Requires-Dist: langchain_openai>=1.1.7
|
|
31
|
+
Requires-Dist: langgraph>=1.0.5
|
|
32
|
+
Requires-Dist: langgraph-checkpoint-postgres>=3.0.2
|
|
33
|
+
Requires-Dist: python-dotenv>=1.1.1
|
|
34
|
+
Requires-Dist: requests<3,>=2.32.5
|
|
35
|
+
Requires-Dist: types-requests>=2.32.4
|
|
36
|
+
Requires-Dist: uri-template<2,>=1.3.0
|
|
37
|
+
Requires-Dist: sqlalchemy>=2.0.45
|
|
38
|
+
Requires-Dist: psycopg[binary]>=3.3.2
|
|
39
|
+
Requires-Dist: contentgrid_extension_helpers>=0.0.3
|
|
40
|
+
Requires-Dist: sqlmodel>=0.0.31
|
|
41
|
+
Requires-Dist: python-multipart>=0.0.21
|
|
42
|
+
Dynamic: license-file
|
|
43
|
+
|
|
44
|
+
# ContentGrid Assistant API
|
|
45
|
+
|
|
46
|
+
A FastAPI framework for building conversational AI assistants with LangGraph integration, designed for the ContentGrid ecosystem.
|
|
47
|
+
|
|
48
|
+
## Overview
|
|
49
|
+
|
|
50
|
+
ContentGrid Assistant API provides a comprehensive framework for creating multi-agent conversational assistants with persistent chat threads, streaming responses, and HAL (Hypertext Application Language) compliant REST APIs. Built on FastAPI and LangGraph, it offers a structured approach to deploying AI agents with built-in authentication, database persistence, and tool calling capabilities.
|
|
51
|
+
|
|
52
|
+
## Key Features
|
|
53
|
+
|
|
54
|
+
- **Multi-Agent Architecture**: Support for multiple independent agents with their own tools and configurations
|
|
55
|
+
- **Thread-Based Conversations**: Persistent conversation threads with PostgreSQL or SQLite storage
|
|
56
|
+
- **LangGraph Integration**: Built-in checkpoint persistence and state management for complex agent workflows
|
|
57
|
+
- **Streaming Support**: Real-time streaming responses for interactive chat experiences
|
|
58
|
+
- **HAL REST APIs**: Hypermedia-driven APIs following HAL specification for discoverability
|
|
59
|
+
- **Authentication**: Integrated ContentGrid user authentication and authorization
|
|
60
|
+
- **File Upload Support**: Handle images, audio, PDFs, and other file attachments in conversations
|
|
61
|
+
- **Tool Calling**: Built-in support for LangChain tools with automatic execution and response handling
|
|
62
|
+
- **Database Flexibility**: PostgreSQL for production or SQLite for development/testing
|
|
63
|
+
- **CORS & Middleware**: Configurable CORS and exception handling middleware
|
|
64
|
+
|
|
65
|
+
## Architecture
|
|
66
|
+
|
|
67
|
+
### Core Components
|
|
68
|
+
|
|
69
|
+
- **ContentGridAssistantAPI**: Specialized FastAPI application with pre-configured routes and middleware
|
|
70
|
+
- **Agent**: Configurable agent with custom tools, authentication, and context management
|
|
71
|
+
- **Thread Management**: CRUD operations for conversation threads with user isolation
|
|
72
|
+
- **Message Handling**: Support for Human, AI, System, and Tool messages with content blocks
|
|
73
|
+
- **Dependency Injection**: Structured dependency resolution for database, authentication, and agent access
|
|
74
|
+
|
|
75
|
+
### API Structure
|
|
76
|
+
|
|
77
|
+
```
|
|
78
|
+
/{agent_name}/
|
|
79
|
+
├── GET / # Agent home with HAL links
|
|
80
|
+
├── GET /tools # List available agent tools
|
|
81
|
+
└── /threads
|
|
82
|
+
├── GET / # List user's threads
|
|
83
|
+
├── POST / # Create new thread
|
|
84
|
+
└── /{thread_id}
|
|
85
|
+
├── GET / # Get thread details
|
|
86
|
+
├── PATCH / # Update thread
|
|
87
|
+
├── DELETE / # Delete thread
|
|
88
|
+
└── /messages
|
|
89
|
+
├── GET / # List messages in thread
|
|
90
|
+
└── POST / # Add message to thread (supports streaming)
|
|
91
|
+
```
|
|
92
|
+
|
|
93
|
+
## Installation
|
|
94
|
+
|
|
95
|
+
```bash
|
|
96
|
+
pip install contentgrid-assistant-api
|
|
97
|
+
```
|
|
98
|
+
|
|
99
|
+
Or install from source:
|
|
100
|
+
|
|
101
|
+
```bash
|
|
102
|
+
git clone <repository-url>
|
|
103
|
+
cd contentgrid-assistant-api/contentgrid_assistant_api
|
|
104
|
+
pip install -e .
|
|
105
|
+
```
|
|
106
|
+
|
|
107
|
+
## Quick Start
|
|
108
|
+
|
|
109
|
+
```python
|
|
110
|
+
from contentgrid_assistant_api.app import ContentGridAssistantAPI
|
|
111
|
+
from contentgrid_assistant_api.types.agents import Agent
|
|
112
|
+
from contentgrid_extension_helpers.authentication.user import ContentGridUser
|
|
113
|
+
|
|
114
|
+
def get_current_user() -> ContentGridUser:
|
|
115
|
+
# Implement your authentication logic
|
|
116
|
+
return ContentGridUser(...)
|
|
117
|
+
|
|
118
|
+
def compile_my_agent(checkpointer):
|
|
119
|
+
# Return your LangGraph compiled agent
|
|
120
|
+
return compiled_graph
|
|
121
|
+
|
|
122
|
+
agents = [
|
|
123
|
+
Agent(
|
|
124
|
+
name="my_agent",
|
|
125
|
+
version="v1.0.0",
|
|
126
|
+
get_current_user_override=get_current_user,
|
|
127
|
+
get_agent_override=compile_my_agent,
|
|
128
|
+
tools=my_tools
|
|
129
|
+
)
|
|
130
|
+
]
|
|
131
|
+
|
|
132
|
+
app = ContentGridAssistantAPI(agents=agents)
|
|
133
|
+
```
|
|
134
|
+
|
|
135
|
+
## How to Use the Library
|
|
136
|
+
|
|
137
|
+
### Step 1: Create Your Tools
|
|
138
|
+
|
|
139
|
+
Define tools that your agent can use. Tools are LangChain-compatible functions decorated with `@tool`:
|
|
140
|
+
|
|
141
|
+
```python
|
|
142
|
+
from langchain_core.tools import tool
|
|
143
|
+
|
|
144
|
+
@tool("get_weather")
|
|
145
|
+
def get_weather(location: str) -> str:
|
|
146
|
+
"""Get the current weather for a location"""
|
|
147
|
+
# Your implementation
|
|
148
|
+
return f"Weather in {location}: Sunny, 72°F"
|
|
149
|
+
|
|
150
|
+
@tool("send_message")
|
|
151
|
+
def send_message(recipient: str, message: str) -> str:
|
|
152
|
+
"""Send a message to a recipient"""
|
|
153
|
+
# Your implementation
|
|
154
|
+
return f"Message sent to {recipient}"
|
|
155
|
+
|
|
156
|
+
tools = [get_weather, send_message]
|
|
157
|
+
```
|
|
158
|
+
|
|
159
|
+
### Step 2: Define Agent Context
|
|
160
|
+
|
|
161
|
+
Create a context class that extends `AgentState` to hold thread-specific data:
|
|
162
|
+
|
|
163
|
+
```python
|
|
164
|
+
from langgraph.graph import MessagesState
|
|
165
|
+
|
|
166
|
+
class ThreadContext(MessagesState):
|
|
167
|
+
"""Context for your agent's conversation thread"""
|
|
168
|
+
user_id: str = None
|
|
169
|
+
conversation_type: str = None
|
|
170
|
+
# Add any other context fields your agent needs
|
|
171
|
+
pass
|
|
172
|
+
```
|
|
173
|
+
|
|
174
|
+
### Step 3: Create the LLM Model
|
|
175
|
+
|
|
176
|
+
Initialize your language model using your preferred provider (OpenAI, Anthropic, Google, etc.):
|
|
177
|
+
|
|
178
|
+
```python
|
|
179
|
+
from langchain_openai import ChatOpenAI
|
|
180
|
+
|
|
181
|
+
model = ChatOpenAI(
|
|
182
|
+
model="gpt-4o",
|
|
183
|
+
api_key="your-api-key"
|
|
184
|
+
)
|
|
185
|
+
model_with_tools = model.bind_tools(tools, parallel_tool_calls=False)
|
|
186
|
+
```
|
|
187
|
+
|
|
188
|
+
### Step 4: Build the Agent Graph
|
|
189
|
+
|
|
190
|
+
Create a LangGraph state graph that defines your agent's workflow:
|
|
191
|
+
|
|
192
|
+
```python
|
|
193
|
+
from langgraph.graph import StateGraph, START, END
|
|
194
|
+
from langgraph.prebuilt import ToolNode
|
|
195
|
+
from langchain_core.messages import SystemMessage, AIMessage
|
|
196
|
+
import typing
|
|
197
|
+
|
|
198
|
+
class AgentState(MessagesState):
|
|
199
|
+
pass
|
|
200
|
+
|
|
201
|
+
tool_node = ToolNode(tools)
|
|
202
|
+
|
|
203
|
+
@typing.no_type_check
|
|
204
|
+
def llm_call(state: AgentState) -> AgentState:
|
|
205
|
+
"""Call the LLM with the current conversation"""
|
|
206
|
+
system_prompt = "You are a helpful assistant"
|
|
207
|
+
messages = state['messages']
|
|
208
|
+
|
|
209
|
+
new_message = model_with_tools.invoke(
|
|
210
|
+
[SystemMessage(content=system_prompt)] + messages
|
|
211
|
+
)
|
|
212
|
+
|
|
213
|
+
state['messages'] = [new_message]
|
|
214
|
+
return state
|
|
215
|
+
|
|
216
|
+
def should_continue(state: AgentState):
|
|
217
|
+
"""Decide if we should call tools or end"""
|
|
218
|
+
messages = state["messages"]
|
|
219
|
+
last_message = messages[-1]
|
|
220
|
+
|
|
221
|
+
if isinstance(last_message, AIMessage) and last_message.tool_calls:
|
|
222
|
+
return "tool_node"
|
|
223
|
+
return END
|
|
224
|
+
|
|
225
|
+
# Build the graph
|
|
226
|
+
agent_builder = StateGraph(AgentState, context_schema=ThreadContext)
|
|
227
|
+
agent_builder.add_node("llm_call", llm_call)
|
|
228
|
+
agent_builder.add_node("tool_node", tool_node)
|
|
229
|
+
agent_builder.add_edge(START, "llm_call")
|
|
230
|
+
agent_builder.add_conditional_edges("llm_call", should_continue, ["tool_node", END])
|
|
231
|
+
agent_builder.add_edge("tool_node", "llm_call")
|
|
232
|
+
|
|
233
|
+
def compile_my_agent(checkpointer):
|
|
234
|
+
"""Compile the agent with checkpointer for persistence"""
|
|
235
|
+
if checkpointer:
|
|
236
|
+
return agent_builder.compile(checkpointer=checkpointer)
|
|
237
|
+
return agent_builder.compile()
|
|
238
|
+
```
|
|
239
|
+
|
|
240
|
+
### Step 5: Initialize the Application
|
|
241
|
+
|
|
242
|
+
Create your FastAPI application with one or more agents:
|
|
243
|
+
|
|
244
|
+
```python
|
|
245
|
+
from contentgrid_assistant_api.app import ContentGridAssistantAPI
|
|
246
|
+
from contentgrid_assistant_api.types.agents import Agent
|
|
247
|
+
from contentgrid_extension_helpers.authentication.user import ContentGridUser
|
|
248
|
+
|
|
249
|
+
def get_current_user() -> ContentGridUser:
|
|
250
|
+
"""Provide authentication context"""
|
|
251
|
+
return ContentGridUser(
|
|
252
|
+
...
|
|
253
|
+
)
|
|
254
|
+
|
|
255
|
+
agents = [
|
|
256
|
+
Agent(
|
|
257
|
+
name="my_assistant",
|
|
258
|
+
version="v1.0.0",
|
|
259
|
+
get_current_user_override=get_current_user,
|
|
260
|
+
get_agent_override=compile_my_agent,
|
|
261
|
+
thread_context=ThreadContext,
|
|
262
|
+
tools=tools
|
|
263
|
+
)
|
|
264
|
+
]
|
|
265
|
+
|
|
266
|
+
app = ContentGridAssistantAPI(agents=agents)
|
|
267
|
+
|
|
268
|
+
if __name__ == "__main__":
|
|
269
|
+
import uvicorn
|
|
270
|
+
uvicorn.run(app, host="0.0.0.0", port=8000, reload=True)
|
|
271
|
+
```
|
|
272
|
+
|
|
273
|
+
### Step 6: Using the API
|
|
274
|
+
|
|
275
|
+
The API provides REST endpoints for each agent. All requests require authentication via the ContentGrid user context. The endpoints follow this pattern: `/api/{agent_name}/...`
|
|
276
|
+
|
|
277
|
+
**Thread Management:**
|
|
278
|
+
- `POST /api/{agent_name}/threads` - Create a new conversation thread
|
|
279
|
+
- `GET /api/{agent_name}/threads` - List all threads for the current user (with pagination)
|
|
280
|
+
- `GET /api/{agent_name}/threads/{thread_id}` - Retrieve a specific thread
|
|
281
|
+
- `PATCH /api/{agent_name}/threads/{thread_id}` - Update thread metadata
|
|
282
|
+
- `DELETE /api/{agent_name}/threads/{thread_id}` - Delete a thread
|
|
283
|
+
|
|
284
|
+
**Message Management:**
|
|
285
|
+
- `GET /api/{agent_name}/threads/{thread_id}/messages` - Retrieve all messages in a thread
|
|
286
|
+
- `POST /api/{agent_name}/threads/{thread_id}/messages` - Send a message to the agent
|
|
287
|
+
|
|
288
|
+
The message endpoint supports two modes:
|
|
289
|
+
- **Non-streaming**: Returns the human message immediately while the agent response is processed in the background
|
|
290
|
+
- **Streaming**: Stream the agent's response in real-time by including the `Accept: text/event-stream` header. The response is sent as server-sent events (SSE) with message chunks and completion signals
|
|
291
|
+
|
|
292
|
+
**File Uploads:**
|
|
293
|
+
When posting a message, you can optionally attach a file.
|
|
294
|
+
The file content is automatically converted into appropriate content blocks and sent to the agent for analysis.
|
|
295
|
+
|
|
296
|
+
### Example: Multi-Agent Server
|
|
297
|
+
|
|
298
|
+
See `example_server/server.py` for a complete example with multiple agents:
|
|
299
|
+
|
|
300
|
+
```python
|
|
301
|
+
from agents.joke_agent.agent import compile_joke_agent
|
|
302
|
+
from agents.order_agent.agent import compile_order_agent
|
|
303
|
+
# ... imports ...
|
|
304
|
+
|
|
305
|
+
agents = [
|
|
306
|
+
Agent(name="joker", version="v0.0.0",
|
|
307
|
+
get_current_user_override=get_dummy_user,
|
|
308
|
+
get_agent_override=compile_joke_agent,
|
|
309
|
+
thread_context=ThreadContext,
|
|
310
|
+
tools=joke_tools),
|
|
311
|
+
Agent(name="order_bot", version="v0.0.0",
|
|
312
|
+
get_current_user_override=get_dummy_user,
|
|
313
|
+
get_agent_override=compile_order_agent,
|
|
314
|
+
thread_context=OrderThreadContext,
|
|
315
|
+
tools=order_tools)
|
|
316
|
+
]
|
|
317
|
+
|
|
318
|
+
app = ContentGridAssistantAPI(agents=agents)
|
|
319
|
+
```
|
|
320
|
+
|
|
321
|
+
This creates two separate conversation assistants accessible at:
|
|
322
|
+
- `/api/joker/threads` - For the joke agent
|
|
323
|
+
- `/api/order_bot/threads` - For the order agent
|
|
324
|
+
|
|
325
|
+
## Configuration
|
|
326
|
+
|
|
327
|
+
### Environment Variables
|
|
328
|
+
|
|
329
|
+
Configure via `.env` file or environment variables:
|
|
330
|
+
|
|
331
|
+
```bash
|
|
332
|
+
# Server Configuration
|
|
333
|
+
SERVER_PORT=8000
|
|
334
|
+
SERVER_URL=http://localhost:8000
|
|
335
|
+
PRODUCTION=false
|
|
336
|
+
WEB_CONCURRENCY=1
|
|
337
|
+
|
|
338
|
+
# Database Configuration
|
|
339
|
+
PG_DBNAME=assistant
|
|
340
|
+
PG_USER=assistant
|
|
341
|
+
PG_PASSWD=assistant
|
|
342
|
+
PG_HOST=postgres
|
|
343
|
+
PG_PORT=5432
|
|
344
|
+
USE_SQLITE_DB=false
|
|
345
|
+
|
|
346
|
+
# Assistant Configuration
|
|
347
|
+
GRAPH_RECURSION_LIMIT=100
|
|
348
|
+
OPENING_MESSAGE="Hello! How can I help you today?"
|
|
349
|
+
|
|
350
|
+
# Path Configuration
|
|
351
|
+
EXTENSION_PATH_PREFIX=/api
|
|
352
|
+
```
|
|
353
|
+
|
|
354
|
+
### Configuration Classes
|
|
355
|
+
|
|
356
|
+
- **AssistantExtensionConfig**: Application-level configuration
|
|
357
|
+
- **DatabaseConfig**: Database connection and initialization settings
|
|
358
|
+
|
|
359
|
+
## Message Types
|
|
360
|
+
|
|
361
|
+
The API supports rich message content including:
|
|
362
|
+
|
|
363
|
+
- **Text**: Plain text messages
|
|
364
|
+
- **Images**: Base64-encoded images with MIME type
|
|
365
|
+
- **Audio**: Audio file attachments
|
|
366
|
+
- **Files**: PDF and other document attachments
|
|
367
|
+
- **Tool Calls**: Agent tool invocations and responses
|
|
368
|
+
|
|
369
|
+
## Database Schema
|
|
370
|
+
|
|
371
|
+
### Threads Table
|
|
372
|
+
- `id`: UUID primary key
|
|
373
|
+
- `name`: Thread name
|
|
374
|
+
- `origin`: Optional origin URL/identifier
|
|
375
|
+
- `component`: Component type (default: datamodel)
|
|
376
|
+
- `user_sub`: User subject identifier
|
|
377
|
+
- `created_at`: Timestamp
|
|
378
|
+
|
|
379
|
+
### Messages
|
|
380
|
+
Messages are stored in LangGraph's checkpoint system with support for:
|
|
381
|
+
- Conversation history
|
|
382
|
+
- Tool call results
|
|
383
|
+
- State snapshots
|
|
384
|
+
- Rollback capabilities
|
|
385
|
+
|
|
386
|
+
## Development
|
|
387
|
+
|
|
388
|
+
### Running Tests
|
|
389
|
+
|
|
390
|
+
```bash
|
|
391
|
+
cd contentgrid_assistant_api
|
|
392
|
+
pytest
|
|
393
|
+
```
|
|
394
|
+
|
|
395
|
+
## License
|
|
396
|
+
|
|
397
|
+
See LICENSE file for details.
|
|
398
|
+
|
|
399
|
+
## Author
|
|
400
|
+
|
|
401
|
+
Ranec Belpaire (ranec.belpaire@xenit.eu)
|