Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add support for Swift Package Manager #1

Open
wants to merge 14 commits into
base: master
Choose a base branch
from
2 changes: 2 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -91,3 +91,5 @@ Network Trash Folder
Temporary Items
.apdisk

.build*

22 changes: 22 additions & 0 deletions .travis.d/before-install.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
#!/bin/bash

if [[ "$TRAVIS_OS_NAME" == "Linux" ]]; then
sudo apt-get -q update
sudo apt-get install -y wget \
clang-3.8 libc6-dev make git libicu52 libicu-dev \
git autoconf libtool pkg-config \
libblocksruntime-dev \
libkqueue-dev \
libpthread-workqueue-dev \
systemtap-sdt-dev \
libbsd-dev libbsd0 libbsd0-dbg \
curl libcurl4-openssl-dev \
libssl-dev \
libedit-dev \
libpython2.7 \
python2.7 python2.7-dev \
libxml2

sudo update-alternatives --quiet --install /usr/bin/clang clang /usr/bin/clang-3.8 100
sudo update-alternatives --quiet --install /usr/bin/clang++ clang++ /usr/bin/clang++-3.8 100
fi
32 changes: 32 additions & 0 deletions .travis.d/install.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
#!/bin/bash

# Install Swift

wget "${SWIFT_SNAPSHOT_NAME}"

TARBALL="`ls swift-*.tar.gz`"
echo "Tarball: $TARBALL"

TARPATH="$PWD/$TARBALL"

cd $HOME # expand Swift tarball in $HOME
tar zx --strip 1 --file=$TARPATH
pwd

export PATH="$PWD/usr/bin:$PATH"
which swift

if [ `which swift` ]; then
echo "Installed Swift: `which swift`"
else
echo "Failed to install Swift?"
exit 42
fi
swift --version


# Environment

TT_SWIFT_BINARY=`which swift`

echo "${TT_SWIFT_BINARY}"
45 changes: 45 additions & 0 deletions .travis.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
language: generic

notifications:
slack: nozeio:LIFY1Jtkx0FRcLq3u1WliHRZ

matrix:
include:
- os: Linux
dist: trusty
env: SWIFT_SNAPSHOT_NAME="https://swift.org/builds/swift-4.0.3-release/ubuntu1404/swift-4.0.3-RELEASE/swift-4.0.3-RELEASE-ubuntu14.04.tar.gz"
sudo: required
- os: Linux
dist: trusty
env: SWIFT_SNAPSHOT_NAME="https://swift.org/builds/swift-4.1-release/ubuntu1404/swift-4.1-RELEASE/swift-4.1-RELEASE-ubuntu14.04.tar.gz"
sudo: required
- os: Linux
dist: trusty
env: SWIFT_SNAPSHOT_NAME="https://swift.org/builds/swift-4.2.3-release/ubuntu1404/swift-4.2.3-RELEASE/swift-4.2.3-RELEASE-ubuntu14.04.tar.gz"
sudo: required
- os: Linux
dist: trusty
env: SWIFT_SNAPSHOT_NAME="https://swift.org/builds/swift-5.0-release/ubuntu1404/swift-5.0-RELEASE/swift-5.0-RELEASE-ubuntu14.04.tar.gz"
sudo: required
- os: osx
osx_image: xcode9
- os: osx
osx_image: xcode9.3
- os: osx
osx_image: xcode10.1
- os: osx
osx_image: xcode10.2

before_install:
- ./.travis.d/before-install.sh

install:
- ./.travis.d/install.sh

script:
- export PATH="$HOME/usr/bin:$PATH"
- export SWIFTENV_ROOT="$HOME/.swiftenv"
- export PATH="${SWIFTENV_ROOT}/bin:${SWIFTENV_ROOT}/shims:$PATH"
- swift build -c release
- swift build -c debug

17 changes: 17 additions & 0 deletions Package.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
// swift-tools-version:4.0

import PackageDescription

let package = Package(
name: "SwiftEliza",
products: [
.library (name: "Eliza", targets: [ "Eliza" ]),
.executable(name: "therapist", targets: [ "therapist" ])
],
dependencies: [
],
targets: [
.target(name: "Eliza", dependencies: []),
.target(name: "therapist", dependencies: [ "Eliza" ])
]
)
17 changes: 17 additions & 0 deletions [email protected]
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
// swift-tools-version:5.0

import PackageDescription

let package = Package(
name: "SwiftEliza",
products: [
.library (name: "Eliza", targets: [ "Eliza" ]),
.executable(name: "therapist", targets: [ "therapist" ])
],
dependencies: [
],
targets: [
.target(name: "Eliza", dependencies: []),
.target(name: "therapist", dependencies: [ "Eliza" ])
]
)
54 changes: 53 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
@@ -1,15 +1,67 @@
# SwiftEliza

![Swift3](https://img.shields.io/badge/swift-3-blue.svg)
![Swift4](https://img.shields.io/badge/swift-4-blue.svg)
![Swift5](https://img.shields.io/badge/swift-5-blue.svg)
![macOS](https://img.shields.io/badge/os-macOS-green.svg?style=flat)
![iOS](https://img.shields.io/badge/os-iOS-green.svg?style=flat)
![tuxOS](https://img.shields.io/badge/os-tuxOS-green.svg?style=flat)
![Travis](https://api.travis-ci.org/AlwaysRightInstitute/SwiftEliza.svg?branch=feature/swift-package-manager&style=flat)

SwiftEliza is a Swift + iOS implementation of Weizenbaum's [ELIZA chatbot](https://en.wikipedia.org/wiki/ELIZA), which is a simulation of a Rogerian psychotherapist.

Based on my [Go implementation](https://github.com/kennysong/goeliza) and QuestionBot from Apple's [App Development with Swift](https://itunes.apple.com/book/app-development-with-swift/id1118575552) book.

![SwiftEliza screenshot](/SwiftEliza/screenshot.png)

# Notes
## Notes

To run, just open the `SwiftEliza.xcodeproj` in XCode.

The functionality of ELIZA is contained in the `Eliza.swift` file, and the rest are boilerplate for the iOS app.

Written in Swift 3 for iOS 10 and above.

## Swift Package Manager

### Import Package

You can import Eliza as a regular Swift Package Manager package. To do so,
use a `Package.swift` like this:

```swift
// swift-tools-version:4.0

import PackageDescription

let package = Package(
name: "MyEliza",
dependencies: [
.package(url: "https://github.com/kennysong/SwiftEliza.git",
from: "1.0.0")
],
targets: [
.target(name: "MyEliza", dependencies: [ "Eliza" ])
]
)
```

### API

The API consists of just three functions:

- `elizaHi() -> String`
- `elizaBye() -> String`
- `replyTo(_: String) -> String`

Example:

```swift
let eliza = Eliza()
print(eliza.elizaHi())
print(eliza.replyTo("Hi Eliza, I'm feeling super-bad today!"))
print(eliza.elizaBye())
```

It is recommended that you add proper delays when responding to interactive
questions. See the iOS app or the [therapist](Sources/therapist) tool for an example.
41 changes: 34 additions & 7 deletions SwiftEliza/Eliza.swift → Sources/Eliza/Eliza.swift
Original file line number Diff line number Diff line change
@@ -1,6 +1,9 @@
import Foundation

struct Eliza {
public struct Eliza {

public init() {}

// A list of introduction sentences for ELIZA.
private let introductions = [
"Hello. How are you feeling today?",
Expand Down Expand Up @@ -281,19 +284,19 @@ struct Eliza {
]

// elizaHi will return a random greeting from Eliza.
func elizaHi() -> String {
public func elizaHi() -> String {
return introductions.randChoice()
}

// elizaBye will return a random goodbye from Eliza.
func elizaBye() -> String {
public func elizaBye() -> String {
return goodbyes.randChoice()
}

// replyTo will construct a reply for a given statement using Eliza's rules.
func replyTo(_ statement: String) -> String {
public func replyTo(_ statement: String) -> String {
// First, preprocess the statement for more effective matching
var statement = preprocess(statement)
let statement = preprocess(statement)

// Then, we check if this is a quit statement
if quitStatements.contains(statement) {
Expand All @@ -305,8 +308,15 @@ struct Eliza {
for (pattern, responses) in psychobabble {
// Apply the regex re to the statement string
let re = try! NSRegularExpression(pattern: pattern)
let matches = re.matches(in: statement, range: NSRange(location: 0, length: statement.characters.count))
#if swift(>=3.2)
let matches = re.matches(in: statement,
range: NSRange(location: 0, length: statement.count))

#else
let matches = re.matches(in: statement,
range: NSRange(location: 0, length: statement.characters.count))
#endif

// If the statement matched any recognizable statements
if matches.count > 0 {
// There should only be one match
Expand All @@ -317,7 +327,13 @@ struct Eliza {
// part of the response, for added realism.
var fragment = ""
if match.numberOfRanges > 1 {
#if os(Linux)
fragment = NSString(string: statement).substring(with: match.range(at: 1))
#elseif swift(>=4.0)
fragment = (statement as NSString).substring(with: match.range(at: 1))
#else
fragment = (statement as NSString).substring(with: match.rangeAt(1))
#endif
fragment = reflect(fragment)
}

Expand Down Expand Up @@ -352,10 +368,21 @@ struct Eliza {
}
}

#if os(Linux)
import SwiftGlibc
#endif

// Add a randChoice method to Array's, which returns a random element.
extension Array {
func randChoice() -> Element {
let index = Int(arc4random_uniform(UInt32(self.count)))
#if os(Linux)
func arc4random_uniform(_ max: UInt32) -> Int32 {
return (SwiftGlibc.rand() % Int32(max-1))
}
let index = Int(arc4random_uniform(UInt32(self.count)))
#else
let index = Int(arc4random_uniform(UInt32(self.count)))
#endif
return self[index]
}
}
46 changes: 46 additions & 0 deletions Sources/therapist/main.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
//
// main.swift
// ElizaTool
//
// Created by Helge Hess on 14.04.18.
//

import Dispatch
import Foundation
import Eliza

var buf = [ Int8 ](repeating: 0, count: 500)

class Therapist {

let Q : DispatchQueue = DispatchQueue(label: "eliza")
var isThinking = false
let thinkingTime : TimeInterval = 2
let eliza = Eliza()

func ask(_ question: String, _ cb: @escaping ( String? ) -> ()) {
guard !isThinking else { return cb(nil) }

isThinking = true
Q.asyncAfter(deadline: .now() + thinkingTime) {
self.isThinking = false
let answer = self.eliza.replyTo(question)
cb(answer)
}
}
}

let eliza = Therapist()
print(eliza.eliza.elizaHi())

repeat {
let cstr = fgets(&buf, 500, stdin)!
let s = String(cString: cstr)

eliza.ask(s) { answer in
guard let answer = answer else { return }
print(answer)
}
}
while true

Loading