Exported source
# importing dependencies
import re
import pandas as pd
from pandas import DataFrame
from pathlib import Path
from spannerlib.utils import load_env
from spannerlib import get_magic_session,Session,Span
This tutorials aim to show how to use the spannerlib framework for simple use cases. To illustrate the simplicity of using spannerlib, all required IE functions will be implemented from scratch.
%%spannerlog
# split documents into sentences with the split ie function
Sents(doc_id,sent)<-
Docs(doc_id,text),split(text)->(sent).
# get all pairs of equal sentences using the eq_content_spans ie function
# make sure we only get unique pairs by using the span_lt ie function to avoid symmetry
EqualSentsUnique(doc_id1,sent1,doc_id2,sent2)<-
Sents(doc_id1,sent1),
Sents(doc_id2,sent2),
span_lt(sent1,sent2)->(True),
eq_content_spans(sent1,sent2)->(True).
In this example, we would like to get a collection of documents. And find identical sentences among them. For example, given the following documents:
input_documents = pd.DataFrame([
('doc1', 'The quick brown fox jumps over the lazy dog. Im walking on Sunshine.'),
('doc2', 'Im walking on Sunshine. Lorem ipsum. Im walking on Sunshine.'),
('doc3', 'All you need is love. The quick brown fox jumps over the lazy dog.'),
])
input_documents
0 | 1 | |
---|---|---|
0 | doc1 | The quick brown fox jumps over the lazy dog. I... |
1 | doc2 | Im walking on Sunshine. Lorem ipsum. Im walkin... |
2 | doc3 | All you need is love. The quick brown fox jump... |
We would like to compute that the first sentence of doc1
is equal to the second sentence of doc3
etc..
To do so, we need 2 IE functions: * One that extracts the Span of all sentences from a document, called split
* The other which would let us know when sentences have identical content but are not actually the same sentence. * this will require them to have the same content when ignoring whitespace * but not be equal spans. Lets call this function eq_content_spans
Lets implement them and register them to our session object. We will use the Span
class to save indexed substrings.
split (text)
# this implementation is naive
# the standard library has a rgx_split ie function that does this in a more efficient way
def split(text):
split_indices = [ pos for pos,char in enumerate(text) if char == '.' ]
start = 0
for pos,char in enumerate(text):
if char == '.':
yield Span(text, start, pos)
start = pos+1
eq_content_spans (span1, span2)
Now let us import our data
Lets make sure we can see our data in using Spannerlog
'?Docs(doc_id,text)'
doc_id | text |
---|---|
doc1 | The quick brown fox jumps over the lazy dog. Im walking on Sunshine. |
doc2 | Im walking on Sunshine. Lorem ipsum. Im walking on Sunshine. |
doc3 | All you need is love. The quick brown fox jumps over the lazy dog. |
Now let us build rules that allow us to find identical sentences:
# this rule gives us
Sents(doc_id,sent)<-
Docs(doc_id,text),split(text)->(sent).
?Sents(doc_id,sent)
# this rule find equal pairs of sentences
EqualSents(doc_id1,sent1,doc_id2,sent2)<-
Sents(doc_id1,sent1),
Sents(doc_id2,sent2),
eq_content_spans(sent1,sent2)->(True).
?EqualSents(doc_id1,sent1,doc_id2,sent2)
'?Sents(doc_id,sent)'
doc_id | sent |
---|---|
doc1 | [@3ba775,0,43) "The quick ..." |
doc1 | [@3ba775,44,67) " Im walkin..." |
doc2 | [@06bc2d,0,22) "Im walking..." |
doc2 | [@06bc2d,23,35) " Lorem ips..." |
doc2 | [@06bc2d,36,59) " Im walkin..." |
doc3 | [@9c32df,0,20) "All you ne..." |
doc3 | [@9c32df,21,65) " The quick..." |
'?EqualSents(doc_id1,sent1,doc_id2,sent2)'
doc_id1 | sent1 | doc_id2 | sent2 |
---|---|---|---|
doc1 | [@3ba775,0,43) "The quick ..." | doc3 | [@9c32df,21,65) " The quick..." |
doc1 | [@3ba775,44,67) " Im walkin..." | doc2 | [@06bc2d,0,22) "Im walking..." |
doc1 | [@3ba775,44,67) " Im walkin..." | doc2 | [@06bc2d,36,59) " Im walkin..." |
doc2 | [@06bc2d,0,22) "Im walking..." | doc1 | [@3ba775,44,67) " Im walkin..." |
doc2 | [@06bc2d,0,22) "Im walking..." | doc2 | [@06bc2d,36,59) " Im walkin..." |
doc2 | [@06bc2d,36,59) " Im walkin..." | doc1 | [@3ba775,44,67) " Im walkin..." |
doc2 | [@06bc2d,36,59) " Im walkin..." | doc2 | [@06bc2d,0,22) "Im walking..." |
doc3 | [@9c32df,21,65) " The quick..." | doc1 | [@3ba775,0,43) "The quick ..." |
Notice that we got each pair twice. This is because while we think of a sentence pair as a set with two sentences, the tuples (x,y)
and (y,x)
are actually different. To remedy this we can limit our pairs to pairs where the first sentence is in a smaller position (ie position in memory).
We can do that by introducing a span_lt
ie function
span_lt (span1, span2)
EqualSentsUniqe(doc_id1,sent1,doc_id2,sent2)<-
Sents(doc_id1,sent1),
Sents(doc_id2,sent2),
span_lt(sent1,sent2)->(True),
eq_content_spans(sent1,sent2)->(True).
?EqualSentsUniqe(doc_id1,sent1,doc_id2,sent2)
'?EqualSentsUniqe(doc_id1,sent1,doc_id2,sent2)'
doc_id1 | sent1 | doc_id2 | sent2 |
---|---|---|---|
doc2 | [@06bc2d,0,22) "Im walking..." | doc1 | [@3ba775,44,67) " Im walkin..." |
doc2 | [@06bc2d,0,22) "Im walking..." | doc2 | [@06bc2d,36,59) " Im walkin..." |
doc2 | [@06bc2d,36,59) " Im walkin..." | doc1 | [@3ba775,44,67) " Im walkin..." |
doc3 | [@9c32df,21,65) " The quick..." | doc1 | [@3ba775,0,43) "The quick ..." |
Most LLM based applications do not include a single call to an LLM, but are rather built using LLM agents. LLM agents are programs that * Wrap LLMs in control logic. * Either deterministic * Or decided by an LLM * Offloads structured reasoning to tools, whose output is fed into some of the Prompts that are used by the agent’s LLMs.
In this tutorial we will show how to build a simple agent that might be used to build the backend of a more nuanced ChatGPT-like like system.
Our agent takes: * A question from a user
And has previous knowledge encoding * How to best answer questions on different topics. * How to a user prefers his answers to be formatted.
For example, given the question “How do we measure the distance between stars” we might note that in cases of scientific questions, it is better to answer using mathematical notation and formulas, rather than pure english. Moreover, some users would like the answer to be styled in an academic way, with dense prose ending with citations, like so:While other users will like a shorter answer in simple language like so:The distance between stars is measured in astronomical units (AU), parsecs (pc), or light-years (ly).
Astronomical Unit (AU): An AU is the average distance between the Earth and the Sun, which is approximately 93 million miles or 150 million kilometers. It is more commonly used to measure distances within our solar system.
Parsec (pc): A parsec is a unit of distance used in astronomy, defined as the distance at which an object would have a parallax angle of one arcsecond. One parsec is equal to approximately 3.26 light-years or 3.09 x 10^13 kilometers.
Light-year (ly): A light-year is the distance that light travels in one year, which is approximately 9.46 trillion kilometers or about 5.88 trillion miles.
To measure the distance to a star, astronomers use methods such as parallax and spectroscopic parallax. Parallax involves observing a star from two different points in Earth’s orbit and measuring the apparent shift in position. Spectroscopic parallax uses the star’s spectral type and luminosity to estimate its distance.
In summary, astronomers use a combination of trigonometric and spectroscopic methods to measure the distance between stars in order to better understand the vastness of our universe.
References: 1. Carroll, B. W., & Ostlie, D. A. (2007). An introduction to modern astrophysics (2nd ed.). Pearson Addison-Wesley. 2. Bennett, J., Donahue, M., Schneider, N., & Voit, M. (2014). The cosmic perspective (7th ed.). Pearson.
One common way to measure the distance between stars is through parallax. Parallax is the apparent shift in position of an object when viewed from two different points.
The parallax angle, denoted by p, is the angle formed between a line from the observer to the nearer star and a line from the observer to the farther star. The parallax angle is related to the distance to the star (d) by the formula:
d = 1 / p
where d is the distance to the star in parsecs (pc) and p is the parallax angle in arcseconds (“).
A parsec is a unit of distance often used in astronomy, equal to about 3.26 light-years. So when we measure the parallax angle of a star, we can use the formula above to calculate its distance from Earth.
So given: * A relation of the form (user,question)
* A relation of the form (topic,topicSpecificIntructions)
* A relation of the form (user,userSpecificInstructions)
We would like to output for each user and question, an answer that fits both the user’s specified preferences and the instructions fitting for that topic.
For building the agent we need 2 basic building blocks as IE functions: * an llm
function of the form llm(model:str,prompt:str)->(answer:str)
* a printf
like function format
for formatting a prompt from template and other strings which will be of the form format(template:str,s_1:str,...s_n:str)->(prompt:str)
The following section will involve technical details such as data type conversions and interfacing with OpenAI’s API. If this is not of interest to the reader, please skip to the next section.
Basic call to LLM
%%spannerlog
model = 'gpt-3.5-turbo'
prompt = "user: sing it with me, love, what is it good for?"
TestLLM(answer)<-
llm($model,$prompt)->(answer).
?TestLLM(answer)
Style and topic based completion
%%spannerlog
model = 'gpt-3.5-turbo'
# asking the llm to generate the topic based on the question
TopicSelection(question ,topic)<-
Questions(user,question),
format($topic_selection_template,question)->(prompt),
llm($model,prompt)->(topic).
# composing a prompt based on topic and user preference
# and calling an llm to generate the answer
StyleBasedQA(question,user,answer)<-
Questions(user,question),
TopicSelection(question,topic),
UserAnswerPreference(user,user_style),
TopicSpecificStyle(topic,topic_style),
format($custom_style_prompt,topic_style,user_style,question)->(prompt),
llm($model,prompt)->(answer).
%%python
sess.export('?StyleBasedQA(question,user,answer)')
Here is the implemetation:
To implement the llm
ie function, we need to wrap some LLM api as an IE function. We want an ie function that takes a string and returns a string. Since most LLM api expect a dict of the form {'role':role,'content':message}
we will write converters that parse them from prompt strings of the form
llm (model, question)
@cache
def _get_client():
return openai.Client()
# we use the rgx_split function to split the string into messages
def _str_to_messages (string_prompt):
return [
{
'role': str(role).replace(': ',''),
'content': str(content)
} for role,content in rgx_split('system:\s|assistant:\s|user:\s', string_prompt.strip())
]
def _messages_to_string(msgs):
return ''.join([f"{msg['content']}" for msg in msgs])
# the specific API we are going to call using the messages interface
def _openai_chat(model, messages):
client = _get_client()
respone = client.chat.completions.create(
model=model,
messages=messages,
seed=42
)
return [dict(respone.choices[0].message)]
# we disk cache our function to spare my openAI credits
@memory.cache
def llm(model, question):
q_msgs = _str_to_messages(question)
a_msgs = _openai_chat(model, q_msgs)
answer = _messages_to_string(a_msgs)
# avoid bad fromatting of the answer in the notebook due to nested code blocks
answer = answer.replace('```','')
return [answer]
['Hello! I am a AI-powered virtual assistant designed to help and provide information to users. How can I assist you today?']
Now we can register the llm function as an ie function and use it from in spannerlog code.
Now let see how we can use spannerlib to build a simple pipeline that varries the LLM prompt in a data dependant way. Imagine that we have a chatbot that we want to act differently based on the topic of conversation and the user preference.
To enable us to format prompts from data, we will use a prompt formatting function with a printf
like syntax. It is available in spannerlib’s stdlib, but we will reproduce it here.
format_ie (f_string, *params)
We will model our data as follows:
# a binary relation that stores how the user prefers the formatted
user_answer_preference = pd.DataFrame([
('Bob', 'please answer in prose. Make sure you add references in the end like a bibliography.'),
('Joe', 'Prose is hard for me to read quickly. Format the answer in bullet points.'),
('Sally', 'Try to avoid complicated jargon and use simple language.'),
])
# relation that stores per topic style
topic_specific_style = pd.DataFrame([
('history', 'please answer in a narrative form, use shakespearing language and lots of examples'),
('science', 'Use mathematical notation and formulas to explain the concepts.'),
('hiphop', 'introduce the answer with a rap verse.'),
('other', 'Be polite and neutral in your answer. Avoid controversial topics.'),
])
sess.import_rel('UserAnswerPreference', user_answer_preference)
sess.import_rel('TopicSpecificStyle', topic_specific_style)
Now we will build our pipeline as follows: * We will make an llm call to help us decide the topic of the question * Based on the topic and the user, we will formulate the final prompt for the llm to answer.
prompts = pd.DataFrame([
('topic_selection',
"""
system: Please select a topic from the following list: [history, science, hiphop, other]
based on the question provided by the user. You are only allowed to say the topic name, nothing else.
user: {}
"""),
('custom_style_prompt',
"""
system: Answer the question of the user in the following style:
topic specific style instructions: {}
user specific style instructions: {}
user: {}
"""
)
])
sess.import_rel('Prompts', prompts)
prompts
0 | 1 | |
---|---|---|
0 | topic_selection | system: Please select a topic from the follow... |
1 | custom_style_prompt | system: Answer the question of the user in th... |
'?Prompts(prompt_id,prompt)'
prompt_id | prompt |
---|---|
custom_style_prompt | system: Answer the question of the user in the following style: topic specific style instructions: {} user specific style instructions: {} user: {} |
topic_selection | system: Please select a topic from the following list: [history, science, hiphop, other] based on the question provided by the user. You are only allowed to say the topic name, nothing else. user: {} |
Now given a relation of user and question
questions= pd.DataFrame([
('Bob', 'Who won the civil war?'),
('Joe', 'Who won the civil war?'),
('Sally', 'Who won the civil war?'),
('Bob', 'How do we measure the distance between stars?'),
('Joe', 'How do we measure the distance between stars?'),
('Sally', 'How do we measure the distance between stars?'),
('Bob', 'Who are the most well known rappers?'),
('Joe', 'Who are the most well known rappers?'),
('Sally', 'Who are the most well known rappers?'),
])
sess.import_rel('Questions', questions)
Now lets combine all of this into a pipeline. We will break our pipeline into more debugable bits, and add some extra logical variables in the rule heads for ease of inspection.
model = 'gpt-3.5-turbo'
TopicPrompt(q,p)<-
Questions(user,q),
Prompts('topic_selection',template),
format(template,q)->(p).
?TopicPrompt(q,p)
TopicSelection(q,t)<-
TopicPrompt(q,p),
llm($model,p)->(t).
?TopicSelection(q,t)
StylePrompt(q,p,topic,user)<-
Questions(user,q),
TopicSelection(q,topic),
UserAnswerPreference(user,user_style),
TopicSpecificStyle(topic,topic_style),
Prompts('custom_style_prompt',prompt_template),
format(prompt_template,topic_style,user_style,q)->(p).
Style_Based_QA(question,topic,user,prompt,answer)<-
StylePrompt(question,prompt,topic,user),
llm($model,prompt)->(answer).
'?TopicPrompt(q,p)'
q | p |
---|---|
How do we measure the distance between stars? | system: Please select a topic from the following list: [history, science, hiphop, other] based on the question provided by the user. You are only allowed to say the topic name, nothing else. user: How do we measure the distance between stars? |
Who are the most well known rappers? | system: Please select a topic from the following list: [history, science, hiphop, other] based on the question provided by the user. You are only allowed to say the topic name, nothing else. user: Who are the most well known rappers? |
Who won the civil war? | system: Please select a topic from the following list: [history, science, hiphop, other] based on the question provided by the user. You are only allowed to say the topic name, nothing else. user: Who won the civil war? |
'?TopicSelection(q,t)'
q | t |
---|---|
How do we measure the distance between stars? | science |
Who are the most well known rappers? | hiphop |
Who won the civil war? | history |
question | topic | user | prompt | answer | |
---|---|---|---|---|---|
0 | How do we measure the distance between stars? | science | Bob | system: Answer the question of the user in th... | The distance between stars is measured in astr... |
1 | How do we measure the distance between stars? | science | Joe | system: Answer the question of the user in th... | - To measure the distance between stars, astro... |
2 | How do we measure the distance between stars? | science | Sally | system: Answer the question of the user in th... | To measure the distance between stars, astrono... |
3 | Who are the most well known rappers? | hiphop | Bob | system: Answer the question of the user in th... | Straight outta Compton, a city so gritty, Let ... |
4 | Who are the most well known rappers? | hiphop | Joe | system: Answer the question of the user in th... | Yo, when it comes to rappers, there's a whole ... |
5 | Who are the most well known rappers? | hiphop | Sally | system: Answer the question of the user in th... | Yo, when it comes to rappers, there's a whole ... |
6 | Who won the civil war? | history | Bob | system: Answer the question of the user in th... | In sooth, fair user, the tale of the Civil War... |
7 | Who won the civil war? | history | Joe | system: Answer the question of the user in th... | - The Civil War, dear user, was a bloody confl... |
8 | Who won the civil war? | history | Sally | system: Answer the question of the user in th... | In sooth, good sir, the tale of the Civil War ... |
And as you can see, we have our agent running.
for (question,topic,user,prompt,answer) in completions.itertuples(name=None, index=False):
print(f"{user}: {question}\nassistant: {answer}\n\n")
print("="*80)
Bob: How do we measure the distance between stars?
assistant: The distance between stars is measured in astronomical units (AU), parsecs (pc), or light-years (ly).
1. **Astronomical Unit (AU)**: An AU is the average distance between the Earth and the Sun, which is approximately 93 million miles or 150 million kilometers. It is more commonly used to measure distances within our solar system.
2. **Parsec (pc)**: A parsec is a unit of distance used in astronomy, defined as the distance at which an object would have a parallax angle of one arcsecond. One parsec is equal to about 3.26 light-years or 3.09 x 10^13 kilometers.
3. **Light-year (ly)**: A light-year is the distance that light travels in one year, which is approximately 9.46 trillion kilometers or about 5.88 trillion miles.
To measure the distance to a star, astronomers use methods such as parallax and spectroscopic parallax. Parallax involves measuring the apparent shift of a star against its background as the Earth orbits the Sun. Spectroscopic parallax measures the star's luminosity and temperature to determine its distance.
In summary, the distance between stars is measured in AU, parsecs, or light-years, using methods such as parallax and spectroscopic techniques.
References:
- "Astronomical Unit." NASA, https://solarsystem.nasa.gov/asteroids-comets-and-meteors/comets/overview/. Accessed 15 Dec. 2022.
- "Parsec." European Southern Observatory, https://www.eso.org/public/usa/. Accessed 15 Dec. 2022.
- "Light-Year." European Southern Observatory, https://www.eso.org/public/usa/. Accessed 15 Dec. 2022.
================================================================================
Joe: How do we measure the distance between stars?
assistant: - To measure the distance between stars, astronomers use a method called parallax, which is based on trigonometry.
- Parallax involves observing a star from two different points in Earth's orbit and measuring the angle between the star and distant background stars.
- By knowing the distance between the two observation points (Earth's orbit), the angle of parallax, and some basic trigonometry, astronomers can calculate the distance to the star.
- The formula used is d = 1 / tan(p), where d is the distance to the star in parsecs and p is the parallax angle in arcseconds.
- By measuring the parallax angle with precision instruments, astronomers can determine the distance to stars within a certain range of accuracy.
================================================================================
Sally: How do we measure the distance between stars?
assistant: To measure the distance between stars, astronomers use a method called parallax. Parallax is the apparent shift in the position of an object when viewed from different angles. With parallax, astronomers measure the angle subtended by the star's position at different points in the Earth's orbit. The distance to the star can be calculated using the formula:
\[ D = \frac{1}{\text{parallax angle}} \]
where \( D \) is the distance to the star. The parallax angle is usually measured in arcseconds. The smaller the parallax angle, the greater the distance to the star. This method allows astronomers to measure distances to nearby stars accurately.
================================================================================
Bob: Who are the most well known rappers?
assistant: Straight outta Compton, a city so gritty,
Let me tell you about the rappers who made it to the top, no pity.
From the East Coast to the West, they've all left a mark,
Icons like Tupac, Biggie, and Jay-Z, they're the ones that spark.
Other legends include Eminem, with his lyrical prowess,
And Kanye West, pushing boundaries, never settling for less.
We can't forget about the women who've paved the way,
Nicki Minaj, Cardi B, Queen Latifah, they slay.
But the rap game is vast, with talents galore,
From Kendrick Lamar to Lil Wayne, they all score.
So if you're looking for names that shine bright,
These rappers will definitely be in sight.
References:
1. Tupac Shakur - Biography. (n.d.). Retrieved from https://www.biography.com/musician/tupac-shakur
2. The Notorious B.I.G. - Biography. (n.d.). Retrieved from https://www.biography.com/musician/the-notorious-big
3. Jay-Z - Biography. (n.d.). Retrieved from https://www.biography.com/musician/jay-z
4. Eminem - Biography. (n.d.). Retrieved from https://www.biography.com/musician/eminem
5. Kanye West - Biography. (n.d.). Retrieved from https://www.biography.com/musician/kanye-west
6. Nicki Minaj - Biography. (n.d.). Retrieved from https://www.biography.com/musician/nicki-minaj
================================================================================
Joe: Who are the most well known rappers?
assistant: Yo, when it comes to rappers, there's a whole crew,
Here are some of the most well-known for you:
- Eminem: Known for his lyrical genius and raw talent
- Jay-Z: Business mogul and rap legend with a smooth flow
- Tupac Shakur: Iconic figure known for his powerful lyrics and impact on the industry
- Notorious B.I.G: Legendary rapper with a unique flow and storytelling abilities
- Kendrick Lamar: Modern-day artist known for his socially conscious lyrics and innovative sound
- Drake: Chart-topping artist with a versatile style and massive commercial success
- Kanye West: Controversial figure known for pushing boundaries and redefining the genre
================================================================================
Sally: Who are the most well known rappers?
assistant: Yo, when it comes to rappers, there's a whole lot in the game,
From OGs like Tupac and Biggie, to new stars on their rise to fame.
Eminem's known for his lyrical prowess and flow,
While Jay-Z's the blueprint for success, yo.
Drake stays topping charts with hits in every season,
And Kendrick Lamar speaks truth, his words have reason.
Don't forget about Cardi B, the queen of the Bronx,
And Nicki Minaj, always bringing heat, never clocks.
These are just a few of the names in the scene,
But there's plenty more with rhymes so mean.
So if you're looking to vibe to some sick beats,
Check out these rappers and feel their heat.
================================================================================
Bob: Who won the civil war?
assistant: In sooth, fair user, the tale of the Civil War is a tumultuous one, with many a battle fought and blood shed ere the ultimate victor emerged. Verily, the conflict betwixt the noble Union forces and the valiant Confederate army didst rage for four long years, from the year of our Lord 1861 to 1865.
'Twas in the year of 1865, that the scales of fortune didst tip in favor of the Union, under the gallant leadership of General Ulysses S. Grant. The Confederate forces, led by General Robert E. Lee, didst valiantly defend their cause, but in the end, they couldst not withstand the might of the Union army.
Thus, with the surrender of General Lee at Appomattox Court House on April 9, 1865, the noble Union forces emerged victorious in this bitter conflict. The unity of the nation was preserved, and the scourge of slavery was banished from the land.
So, dear user, it canst be said that the Civil War was won by the Union forces, securing the future of a united and free America.
References:
1. McPherson, James M. "Battle Cry of Freedom: The Civil War Era." Oxford University Press, 1988.
2. Foote, Shelby. "The Civil War: A Narrative." Vintage Books, 1986.
================================================================================
Joe: Who won the civil war?
assistant: - The Civil War, dear user, was a bloody conflict
- In this great struggle, Armies clashed and did afflict
- 'Twas the blue-clad Union and the gray Confederate band
- Seeking victory and power over this vast land
- As the years dragged on, battles were fiercely fought
- From Gettysburg to Antietam, each victory dearly bought
- But in the end, the Union did prevail
- And the Confederacy's hopes did in sorrow pale
- Abraham Lincoln, the great President of the North
- Led his people through this war, showing his worth
- With Grant and Sherman by his side
- They turned the tide, their foes to chide
- So it was the Union, with its starry flag unfurled
- Who emerged victorious, in this tumultuous world
- The Confederacy, defeated and forlorn
- Surrendered at Appomattox, their dreams torn
- And so, dear user, the answer is clear
- The Union won the Civil War, without fear
- But the scars of this conflict still remain
- In the history of our nation, causing lasting pain
================================================================================
Sally: Who won the civil war?
assistant: In sooth, good sir, the tale of the Civil War is a tragic and tumultuous one, fraught with strife and sorrow. 'Twas a conflict of great import that pitted brother against brother and father against son, tearing asunder the very fabric of our nation.
As for the victors of this bloody conflict, it was the Union forces led by General Ulysses S. Grant who emerged triumphant over the Confederate army under General Robert E. Lee. 'Twas in the spring of 1865, at the Battle of Appomattox Court House, that General Lee surrendered his sword to General Grant, marking the end of the war and the preservation of the Union.
'Tis a tale of great tragedy and triumph, of sacrifice and suffering, but in the end, the Union was preserved and the promise of liberty and equality for all was upheld. Let us not forget the lessons learned from this dark chapter in our nation's history, and let us strive to heal the wounds of the past and work towards a brighter future for all.
================================================================================